Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

Создание Custom Scope в JEE и Spring

Anna | 1.06.2014 | нет комментариев
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 – бин. Ключом в Мар бинов является объект ContextualContextual<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)

1-й применяется для возвращения теснее сделанного объекта, сохраненного в кеше. Если данный способ вернул null, то вызывается 2-й, тот, что теснее изготавливает создание нового экземпляра бина.

@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 и resolveContextualObjectresolveContextualObject не упоминаются в документации 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>
    <property name="scopes">
        <map>
            <entry key="WsScope">
                <bean />
            </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></bean>
		</jaxws:handlers>
</jaxws:endpoint>
<bean id="testWS"></bean>

Вследствие тому, что сервис и хендлер определены как спринговые бины, @Autowired в них работает.

Завершение

Как мы можем видеть сделать custom scope в JEE и Spring довольно легко и фактически идентично. Соответсвующие интерфейсы во многом сходны. Только в JEE, на мой взор, реализация больше целостная — все способы ясно для чего и ясно когда вызываются, и больше надёжная — JEE обеспецивает удаление каждой иерархии заинжекченных объектов.

Источник: programmingmaster.ru

Оставить комментарий

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

Вы можете использовать это HTMLтеги и атрибуты: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2014 © BB3x.ru - русская поддержка форума phpBB