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. Архитектура в обоих случаях одинаковая:
В Spring для создания веб-сервиса будем использовать Apache CXF, чтобы было минимум отличий от JEE.
Контекст предназначен для генерации/хранения/деактивации бинов нашего scope. Каждая сессия в нашем скопе идентифицируется специальным id, который хранится в ThreadLocal переменной. Контекст читает этот id возвращает экземпляры бинов соответствующих текущей сессии, которые хранятся локально в объекте класса Map. Т.е. у каждого потока sessionId будет иметь свое значение и контекст будет возвращать соответствующие экземпляры бинов. Соответственно, контекст содержит методы для активации и деактивации сессии. Теперь об этом более подробно. Помимо методов для активации и декативации в JEE нам надо реализовать интерфейс
Здесь
Создание 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 – это интерфейс, используемый CDI для создаения и удаления бинов. Грубо говоря, CDI опереирует не нашими конкретными бинами типа T, а конкретными реализациями Contextual (Bean, Decorator, Interceptor)
В 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 обеспецивает удаление всей иерархии заинжекченных объектов.