Создание 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<T> { public CreationalContext<T> ctx; public T instance; } private Map<String, Map<Contextual, InstanceInfo>> instances = new HashMap<SessionId, Map<Contextual, InstanceInfo>>();
Здесь instances — это Map, где ключом является id сессии, а значанием Map бинов этой сессии. Но просто ссылки на бин нам недостаточно. При деактивации бина CDI надо знать контекст, в котором данный бин был создан, поэтому и исползуется класс InstanceInfo, в ктором ctx – контекст, а instance – бин. Ключом в Мар бинов является объект Contextual. Contextual<T> – это интерфейс, используемый CDI для создаения и удаления бинов. Грубо говоря, CDI опереирует не нашими конкретными бинами типа T, а конкретными реализациями Contextual<T> (Bean<T>, Decorator<T>, Interceptor<T>)
В Spring:
private Map<String, Map<String, Object>> instances = new HashMap<String, Map<String, Object>>();
Как видно, Spring оперирует объектами напрямую.
Установка текущей сессии.
Как уже говорилось выше, id текущей сессии хранится в ThreadLocal переменной. В Springи JEE это делается одинаково.
private final ThreadLocal<String> currentSessionId = new ThreadLocal<String>() { 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<Contextual, InstanceInfo> map = new HashMap<Contextual, InstanceInfo>(); 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<Contextual, InstanceInfo> map = instances.get(id); if (map == null) { throw new RuntimeException("WsScope with id =" + id + " doesn't exist"); } Set<Contextual> 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<String, Object> map = instances.get(id); if (map == null) { throw new RuntimeException("WsScope with id =" + id + " doesn't exist"); } Map<String, Object> objectsMap = instances.get(id); Set<String> 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<String, Object> 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<String, Runnable> 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> T get(Contextual<T> contextual), и public <T> T get(Contextual<T> contextual, CreationalContext<T> creationalContext)
Первый используется для возвращения уже созданного объекта, сохраненного в кеше. Если этот метод вернул null, то вызывается второй, который уже производит создание нового экземпляра бина.
@Override public <T> T get(Contextual<T> contextual) { Map<Contextual,InstanceInfo> map = instances.get(currentSessionId.get()); if (map == null) { return null; } InstanceInfo<T> info = map.get(contextual); if (info == null) { return null; } return info.instance; } @Override public <T> T get(Contextual<T> contextual, CreationalContext<T> creationalContext) { T instance = contextual.create(creationalContext); InstanceInfo<T> info = new InstanceInfo<T>(); info.ctx = creationalContext; info.instance = instance; Map<Contextual, InstanceInfo> map = nstances.get(currentSessionId.get()); if (map == null) { map= new HashMap<Contextual, Context.InstanceInfo>(); 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<String, Object> 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<String, Object> 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. В нашем случае используем файл конфигурации
<bean class="org.springframework.beans.factory.config.CustomScopeConfigurer"> <property name="scopes"> <map> <entry key="WsScope"> <bean class="com.dataart.customscope.spring.context.WsScope" /> </entry> </map> </property> </bean>
В данном случае наш контекст определен как спринговый бин со 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<SOAPMessageContext> { 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<QName> getHeaders() { return null; } }
В Spring
В Spring код практически такой же. Только вместо @Inject используется @Autowired, по другому определяется сервис и по-другому подключается веб-сервис и хендлер.
Определение сервиса:
@Service @Scope(value = "WsScope", proxyMode = ScopedProxyMode.TARGET_CLASS) public class WsService { ... }
Обратите внимание — proxyMode = ScopedProxyMode.TARGET_CLASS обязательно! Дело в том, что нам нельзя инжектить прямую ссылку на наш сервис, т.к. экземпляр веб-сервиса один, а экземпляров сервиса много. И нам нужен прокси объект, через которой мы будем получать ссылку на соответсвующий сервис.
Регистрация веб-сервиса и хендлера:
<jaxws:endpoint id="testWsService" implementor="#testWS" address="/WsTest" publish="true"> <jaxws:handlers> <bean class="com.dataart.customscope.spring.context.WsSoapHandler"></bean> </jaxws:handlers> </jaxws:endpoint> <bean id="testWS" class="com.dataart.customscope.spring.testapp.WsTest"></bean>
Благодаря тому, что сервис и хендлер определены как спринговые бины, @Autowired в них работает.
Заключение
Как мы можем видеть создать custom scope в JEE и Spring достаточно просто и практически одинаково. Соответсвующие интерфейсы во многом сходны. Только в JEE, на мой взгляд, реализация более целостная — все методы понятно для чего и понятно когда вызываются, и более надёжная — JEE обеспецивает удаление всей иерархии заинжекченных объектов.
ссылка на оригинал статьи http://habrahabr.ru/post/220233/
Добавить комментарий