Создание 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<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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *