[Из песочницы] Создание Custom Scope в JEE и Spring

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 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