[Из песочницы] Создание Custom Scope в JEE и Spring23.04.2014 11:07
Scope определяет жизненный цикл объекта. Например, java-бин (далее просто бин) определённый в RequestScope создается при получении http запроса и освобождается при завершении данного запроса. В JEE и в Spring есть возможность создавать свой собственный scope. Т.е. мы можем создавать объекты со своим собственным жизненным циклом — они будут создаваться по какому либо нашему событию и также уничтожаться. В JEE за это отвечает спецификация CDI (Context and Dependency Injection) и на самом деле там уже есть один подобный встроенный scope. Это ConversationScope. У нас есть API и аннотации для начала и окончания conversation. Если мы их не используем, то по-умолчанию ConversationScope ведет себя как RequestScope. Для отслеживания conversation каждого отдельного клиента используется специальный conversationId, который обычно добавляется как параметр http запроса. Но такой подход не работает для веб-сервисов. А в Spring вообще нет ничего подобного. Но в моей практике заказчик попросил сделать веб-сервис, который бы использовал одно и то же физическое соединение к внешней системе для нескольких последовательных вызовов. Также надо было хранить некоторое количество дополнительных данных. Т.е. надо было сохранять некое состояние (объект с соединением и данными) на определённый промежуток времени, такой аналог conversation scope для веб-сервиса. Можно, конечно, сохранить этот объект в Мар, где ключом будет наш аналог conversationId, а Мар положить в ServleContext и доставать это всё из методов веб-сервиса. Но это неудобно. Гораздо удобнее, когда сам сервер будет инжектить нам наш объект по заданному conversationId. Поэтому, сделаем свой scope, который будет работать с SOAP веб-сервисом. Сам по себе веб-сервис не может принадлежать какому-либо scope, но наш бин, который мы будем инжектить в веб-сервис, будет принадлежать нашему scope.Создание CustomScope для Spring и JEE практически одинаково. Для примера рассмотрим создание следующего приложения: у нашего веб-сервиса будет метод, который активизирует наш scope и возвращает sessionId (аналог conversationId). Затем, используя данный id мы вызовем метод, который сохранит данные в нашем scope. Потом мы вызовем метод, который эти данные прочитает, а потом закроем scope. Архитектура в обоих случаях одинаковая: Создается и регистрируется класс ответственный за создание бинов нашего scope.
Создается SOAP Handler, который перехватывает параметр sessionId и устанавливает состояние scope для текущего потока.
Создается веб-сервис, который содержит методы для активации и деактивации scope
.В Spring для создания веб-сервиса будем использовать Apache CXF, чтобы было минимум отличий от JEE.Создание класса контекста scope.
Контекст предназначен для генерации/хранения/деактивации бинов нашего scope. Каждая сессия в нашем скопе идентифицируется специальным id, который хранится в ThreadLocal переменной. Контекст читает этот id возвращает экземпляры бинов соответствующих текущей сессии, которые хранятся локально в объекте класса Map. Т.е. у каждого потока sessionId будет иметь свое значение и контекст будет возвращать соответствующие экземпляры бинов. Соответственно, контекст содержит методы для активации и деактивации сессии. Теперь об этом более подробно. Помимо методов для активации и декативации в JEE нам надо реализовать интерфейс javax.enterprise.context.spi.Context, в Spring — org.springframework.beans.factory.config.Scope. Эти интерфейсы похожи, поэтому и реализации тоже очень похожи. Для JEE сделаем класс WsContext, для Spring — WsScope. Они состоят из следующих частей: Хранения бинов сессии
в JEE:
private static class InstanceInfo {
public CreationalContext ctx;
public T instance;
}
private Map> instances =
new HashMap>();
Здесь instances — это Map, где ключом является id сессии, а значанием Map бинов этой сессии. Но просто ссылки на бин нам недостаточно. При деактивации бина CDI надо знать контекст, в котором данный бин был создан, поэтому и исползуется класс InstanceInfo, в ктором ctx — контекст, а instance — бин. Ключом в Мар бинов является объект Contextual. Contextual — это интерфейс, используемый CDI для создаения и удаления бинов. Грубо говоря, CDI опереирует не нашими конкретными бинами типа T, а конкретными реализациями Contextual (Bean, Decorator, Interceptor)В Spring:
private Map> instances = new HashMap>();
Как видно, Spring оперирует объектами напрямую.Установка текущей сессии.
Как уже говорилось выше, id текущей сессии хранится в ThreadLocal переменной. В Springи JEE это делается одинаково.
private final ThreadLocal currentSessionId = new ThreadLocal() {
protected String initialValue () {
return null;
}
};
public String getCurrentSessionId () {
return currentSessionId.get ();
}
public void setCurrentSessionId (String currentSessionId) {
this.currentSessionId.set (currentSessionId);
}
Активация сессии
Также одинаково для JEE и Spring. Здесь мы просто создаем пустую Map для id сессии.
public void activate (String sessionId) {
Map map = new HashMap();
instances.put (sessionId, map);
this.currentSessionId.set (sessionId);
}
В JEE дополнительно требуется реализация метода для проверки активности контекста, JEE вызывает этот метод перед обращением к контексту:
@Override
public boolean isActive () {
String id = currentSessionId.get ();
return instances.containsKey (id);
}
Деактивация сессии
В JEE
public void deactivate () {
String id = currentSessionId.get ();
Map map = instances.get (id);
if (map == null) {
throw new RuntimeException («WsScope with id =» + id + » doesn’t exist»);
}
Set keySet = map.keySet ();
for (Contextual contextual: keySet) {
InstanceInfo instanceInfo = map.get (contextual);
contextual.destroy (instanceInfo.instance, instanceInfo.ctx);
}
currentSessionId.set (null);
instances.remove (id);
}
Здесь мы просим JEE удалить все бины, которые были созданы в нашей сессии. Под удалением понимается вызов метода с аннотацией @PreDestroy и делание бина доступным для garbage collector. JEE гарантирует, что другие бины, которые были заинжекчены в наши, будут корректно удалены при необходимости.В Spring
Всё примерно точно также:
public void deactivate () {
String id = currentSessionId.get ();
Thread currentThread = Thread.currentThread ();
Map map = instances.get (id);
if (map == null) {
throw new RuntimeException («WsScope with id =» + id + » doesn’t exist»);
}
Map objectsMap = instances.get (id);
Set keySet = objectsMap.keySet ();
for (String name: keySet) {
remove (name);
}
instances.remove (id);
currentSessionId.set (null);
}
В отличии от JEE, в Spring нам надо реализовать метод remove для удаления бинов. Этот метод объявлен в интерфейсе Scope, но вызывать его мы должны сами.
public Object remove (String name) {
String sessionId = currentSessionId.get ();
if (sessionId == null) {
throw new RuntimeException («WsScope is inactive»);
}
Map map = instances.get (sessionId);
if (map == null) {
throw new RuntimeException («WsScope is inactive»);
}
Runnable runnable = destructionCollbacks.get (name);
Thread t = new Thread (runnable);
t.start ();
return map.remove (name);
}
destructionCallbacks определен следующим образом:
private Map destructionCollbacks = new HashMap<>();
Эта Мар инициализируется в другом методе из интерфейса Scope
public void registerDestructionCallback (String name, Runnable callback) {
destructionCollbacks.put (name, callback);
}
Как я заметил, callback, который нам передает Spring, удаляет только объект, имя которого было передано в registerDestructionCallback. Объекты заинжекченные в данный объект, в отличии от JEE, не удаляются. Т.е. Надо быть осторожными с инжектом в бины из custom scope в Spring.Создание бинов
В JEE
Для этого используются методы
public T get (Contextual contextual), и
public T get (Contextual contextual, CreationalContext creationalContext)
Первый используется для возвращения уже созданного объекта, сохраненного в кеше. Если этот метод вернул null, то вызывается второй, который уже производит создание нового экземпляра бина.
@Override
public T get (Contextual contextual) {
Map map = instances.get (currentSessionId.get ());
if (map == null) {
return null;
}
InstanceInfo info = map.get (contextual);
if (info == null) {
return null;
}
return info.instance;
}
@Override
public T get (Contextual contextual, CreationalContext creationalContext) {
T instance = contextual.create (creationalContext);
InstanceInfo info = new InstanceInfo();
info.ctx = creationalContext;
info.instance = instance;
Map map = nstances.get (currentSessionId.get ());
if (map == null) {
map= new HashMap();
instances.put (currentSessionId.get (), map);
}
map.put (contextual, info);
return instance;
}
В Spring
В Spring есть похожие методы get и resolveContextualObject. resolveContextualObject не упоминаются в документации Spring по созданию custom scope. Установка брейкпоинтов и запуск в дебаггере показала, что этот метод даже не вызывается. Гугл показал, что обычно этот метод не реализуется, т.е. возвращает null. Но мы всё равно его реализуем и вызовем сами из метода get. Это сделает get более читабельным.
public Object get (String name, ObjectFactory> objectFactory) {
Object object = resolveContextualObject (name);
if (object!= null) {
return object;
}
String sessionId = currentSessionId.get ();
if (sessionId == null) {
throw new RuntimeException («WsScope is inactive»);
}
Map map = instances.get (sessionId);
if (map == null) {
throw new RuntimeException («WsScope is inactive»);
}
object = objectFactory.getObject ();
map.put (name, object);
return object;
}
public Object resolveContextualObject (String name) {
String sessionId = currentSessionId.get ();
if (sessionId == null) {
return null;
}
Map map = instances.get (sessionId);
if (map == null) {
return null;
}
Object object = map.get (name);
return object;
}
Также в org.springframework.beans.factory.config.Scope есть ещё один такой же невызываемый метод: public String getConversationId (). Этот метод опциональный, но в нашем случае, согласно javadoc, у нас есть всё необходимое для его реализации.
public String getConversationId () {
return currentSessionId.get ();
}
Определение scope
В JEE
В JEE нам нужна аннотация, которой мы будем помечать объекты, которые мы хотим создавать в нашем scope.
@Target ({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
@Retention (RetentionPolicy.RUNTIME)
@NormalScope
public @interface WsScope {
}
И ещё нам осталось связать наш контекст с нашим scope. Для этого в контексте есть специальный метод:
@Override
public Class extends Annotation> getScope () {
return WsScope.class;
}
В Spring
В Spring scope определяется просто именем, которое дается ему при регистрации, как это делается будет описано ниже.Регистрация контекста (scope)
В JEE
В JEE контекст регистрируется при помощи механизма CDI Extension. Сначала надо создать класс, реализующий Extension и перекрыть метод
public void afterBeanDiscovery (@Observes AfterBeanDiscovery abd, BeanManager bm)
В нем контекст создается и регистрируется:
context = new WsContext ();
abd.addContext (context);
Класс extension регистрируется в простом текстовом файле /META-INF/services/javax.enterprise.inject.spi.Extension. Надо просто прописать полное имя класса extension в этом файле.Наш класс Extension полностью:
public class WsExtension implements Extension {
private WsContext context;
public WsContext getContext () {
return context;
}
public void afterBeanDiscovery (@Observes AfterBeanDiscovery abd, BeanManager bm) {
context = new WsContext ();
abd.addContext (context);
}
}
В Spring
В Spring есть несколько способов регистрации scope. В нашем случае используем файл конфигурации
В данном случае наш контекст определен как спринговый бин со scope Singleton.Сохранение ссылки на контекст
Чтобы вызывать методы активации и закрытия scope нам надо иметь ссылку на наш контекст.В JEE
В JEE как видно из предыдущего пункта, мы сохранили ссылку на контекст в классе WsExtension. Этот класс можно инжектить в любой другой объект, хоть он и не принадлежит ни одному из встроенных scope. Но инжектить непосредственно WsContext удобнее чем Extension. Для этого сделаем класс Producer:
public class WsContextProducer {
@Inject
private WsExtension ext;
@Produces
public WsContext getContext () {
return ext.getContext ();
}
}
Но наш класс контекста сам по себе удовлетворяет требованиям manged bean и JEE может инжектить его в другие бины со scope Default (при каждом инжекте будет создаваться новый экземпляр). Получилось, что мы сделали конфликт — CDI может создать WsScope двумя способами: default и через Producer. А нам надо инжектить наш контекст, который мы создали в экстеншене, т.е. через Producer. Поэтому нам надо сделать так, чтобы CDI не воспринимал наш контекст как бин. В JEE7 для этого есть аннотация @Vetoed. Т.е. наш контекст выглядит так:
@Vetoed
public class WsContext implements Context {…}
Теперь мы можем инжектить наш контекст куда хотим при помощи такого кода:
@Inject
private WsContext context;
В Spring
Т.к. мы определили scope как спринговый бин, то мы можем инжектить его как обычно:
@Autowired
private WsScope scope;
Использование нашего scope
Веб-сервис, который хочет работать в режиме сессии передает id сессии в параметре ws-session-id. Все запросы от нашего веб-сервиса обрабатываются специальным хендлером, который читает данный id и устанавливает его в наш контекст для текущего потока. Т.е. для данного потока наш контекст становится активным. Если id нет, или это id не находится в нашем контексте (не был активирован), то при попытке получить объект из нашего контекста сервером будет выброшено исключение. Для активации id в контексте, нам надо вызвать метод activate () нашего контекста. Он сгенерирует id, активирует его и вернет клиенту. Для этого мы сделаем в веб-сервисе метод, который вызовет этот метод. Для деактивации сделаем аналогично с методом deactivate (). В веб-сервис мы инжектим сервис (простой бин WsService) который создан в нашем scope. Этот сервис и содержит состояние между различными вызовами методов веб-сервиса. Т.е. в зависимости от id сессии в наш веб-сервис будут попадать различные экземпляры сервиса, соответствующие данному id.В JEE
@WsScope
public class WsService {
…
}
Код веб-сервиса:
@WebService ()
@HandlerChain (file = «wshandler.xml», name = »)
public class WsScopeTest {
private static int id = 0;
@Inject
private WsContext context;
@Inject
private WsService srv;
@WebMethod ()
public String startWsScope () {
String sessionId = String.valueOf (id++);
context.activate (sessionId);
return sessionId;
}
@WebMethod ()
public void endWsScope (@WebParam (name = «ws-session-id») String sessionId) {
context.deactivate ();
}
@WebMethod ()
public void setName (@WebParam (name = «ws-session-id») String sessionId,
@WebParam (name = «name»)String name) {
srv.setName (name);
}
@WebMethod ()
public String sayHello (@WebParam (name = «ws-session-id») String sessionId) {
return srv.hello ();
}
}
Код хэндлера:
public class WsCdiSoapHandler implements SOAPHandler {
private static final Logger LOGGER = Logger.getLogger (WsCdiSoapHandler.class.getName ());
@Inject
private WsContext context;
@Override
public void close (MessageContext ctx) {
}
@Override
public boolean handleFault (SOAPMessageContext ctx) {
return true;
}
@Override
public boolean handleMessage (SOAPMessageContext ctx) {
Boolean outbound = (Boolean) ctx.get (MessageContext.MESSAGE_OUTBOUND_PROPERTY);
SOAPMessage message = ctx.getMessage ();
SOAPBody soapBody;
try {
soapBody = message.getSOAPBody ();
} catch (SOAPException e) {
e.printStackTrace ();
return false;
}
String methodName = null;
NodeList nodes = soapBody.getChildNodes ();
methodName = findMethodName (methodName, nodes);
if (outbound) {
LOGGER.fine (»[OUT] » + methodName.replace («Response»,»));
return true;
}
LOGGER.fine (»[IN] » + methodName);
String sessionId = findSessionId (nodes);
context.setCurrentSessionId (sessionId);
LOGGER.fine («Handler. Id=» + sessionId);
return true;
}
private String findMethodName (String methodName, NodeList nodes) {
for (int i = 0; i < nodes.getLength(); i++) {
Node node = nodes.item(i);
if (Node.ELEMENT_NODE == node.getNodeType()) {
methodName = node.getLocalName();
}
}
return methodName;
}
private String findSessionId (NodeList nodes) {
for (int i = 0; i < nodes.getLength(); i++) {
Node node = nodes.item(i);
if ("ws-session-id".equals(node.getLocalName())) {
Node firstChild = node.getFirstChild();
if (firstChild == null) {
return null;
}
return firstChild.getNodeValue();
}
NodeList childNodes = node.getChildNodes();
String id = findSessionId(childNodes);
if (id != null) {
return id;
}
}
return null;
}
@Override
public Set getHeaders () {
return null;
}
}
В Spring
В Spring код практически такой же. Только вместо @Inject используется @Autowired, по другому определяется сервис и по-другому подключается веб-сервис и хендлер.Определение сервиса:
@Service
@Scope (value = «WsScope», proxyMode = ScopedProxyMode.TARGET_CLASS)
public class WsService {
…
}
Обратите внимание — proxyMode = ScopedProxyMode.TARGET_CLASS обязательно! Дело в том, что нам нельзя инжектить прямую ссылку на наш сервис, т.к. экземпляр веб-сервиса один, а экземпляров сервиса много. И нам нужен прокси объект, через которой мы будем получать ссылку на соответсвующий сервис.Регистрация веб-сервиса и хендлера:
Благодаря тому, что сервис и хендлер определены как спринговые бины, @Autowired в них работает.Заключение
Как мы можем видеть создать custom scope в JEE и Spring достаточно просто и практически одинаково. Соответсвующие интерфейсы во многом сходны. Только в JEE, на мой взгляд, реализация более целостная — все методы понятно для чего и понятно когда вызываются, и более надёжная — JEE обеспецивает удаление всей иерархии заинжекченных объектов.
© Habrahabr.ru