Я работал над созданием веб-приложения CGI / Perl с сохранением состояния в интерфейсе RESTful без сохранения состояния с использованием JAX-RS 2. Помимо механизмов взаимодействия с веб-приложением CGI / Perl (подумайте о разумном использовании клиентов HTTP и просмотре HTML), одним из сложных аспектов было обеспечение того, чтобы одновременные запросы к службе REST не заканчивались общим сеансом приложения CGI / Perl.
Каждый запрос должен установить и завершить сеанс для конкретного пользователя веб-приложения; любые входящие запросы в течение этого времени не должны устанавливать сеанс с тем же пользователем.
Моя первоначальная идея состояла в том, чтобы настроить исполнителя пула потоков Tomcat так, чтобы каждый поток был привязан к определенному пользователю. Я сделал прогресс в этой области, но отказался от подхода после борьбы с недостаточно документированными tomcat7-maven-plugin
. Кроме того, я опасался быть связанным с Tomcat на таком низком уровне, учитывая, что мой обычай org.apache.catalina.Executor
«заимствовал» из стандартной реализации .
Подход, на котором я остановился, состоит из следующих компонентов:
- Проект Spring Boot + Jersey 2 на базе Maven с двумя субмодулями
- Библиотека взаимодействия веб-приложений CGI / Perl
- Веб-сервис JAX-RS, предоставляющий интерфейс RESTful вокруг этой библиотеки
- A
java.util.concurrent.BlockingQueue
для управления доступом потоков к пулу ограниченных идентификаторов пользователей, а также настраиваемый фильтр сервлетов для управления очередью. - Пользовательский поставщик инъекции , ответственный за инъекционные заседания на JAX-RS ресурсы через
@Context
или@Inject
аннотации.
Maven, Spring Boot и JAX-RS 2 с платформой из Джерси
Вот конфигурация POM для компонента JAX-RS (родительский POM и POM для библиотеки взаимодействия не интересны и не имеют отношения к рассматриваемому вопросу).
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven. apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.example.bdkosher/groupId>
<artifactId>cgi-app-wrapper</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>cgi-app-rest-api</artifactId>
<packaging>war</packaging>
<properties>
<spring-boot.version>1.2.4.RELEASE</spring-boot.version>
<java.version>1.7</java.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jersey</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>cgi-app-interaction-library</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
</dependencies>
<build>
<finalName>cgiwrapp</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>2.6</version>
<configuration>
<failOnMissingWebXml>false</failOnMissingWebXml>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
</plugin>
</plugins>
</build>
</project>
Важные области, чтобы выделить:
- Поскольку у меня был собственный родительский POM, я использовал подход импорта POM dependencyManagement для извлечения зависимостей Spring Boot.
- Я настроил плагин maven-war так, чтобы он не использовался,
failOnMissingWebXml
поскольку я использовал аннотации для настройки приложения Servlet и не хотел создавать пустойweb.xml
Я определил свой пул идентификаторов пользователей приложения CGI / Perl в src/main/resources/application.properties
файле
cgiapp.users=user1,user2,bob
и создал пользовательский класс конфигурации для извлечения этих пользователей как a, java.util.Set
чтобы избежать проблем, если один и тот же идентификатор пользователя был указан несколько раз:
import com.google.common.base.Splitter;
import static org.apache.commons.collections.IteratorUtils.toList;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
public class MyAppConfig {
@Value("${cgiapp.users}")
private String userIdsConf;
public Set<String> getUserIds() {
return userIdsConf == null ? null
: new HashSet<String>(toList(Splitter.on(',').trimResults().split(userIdsConf).iterator()));
}
}
Помечая этот класс как @Component
, я мог положиться на Spring для управления и внедрения этой конфигурации в другие bean-компоненты.
Наконец, я загрузил свое веб-приложение, используя собственное ResourceConfig
расширение Джерси , украшенное Spring Boot.
import com.example.bdkosher.restapi.AppSessionInjectionFactory;
import com.example.bdkosher.AppSession;
import javax.ws.rs.ApplicationPath;
import org.glassfish.hk2.utilities.binding.AbstractBinder;
import org.glassfish.jersey.server.ResourceConfig;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.stereotype.Component;
@ApplicationPath("cgiwrapp")
@Component
@SpringBootApplication
public class CgiWrapplication extends ResourceConfig {
public CgiWrapplication() {
register(new AbstractBinder() {
@Override
protected void configure() {
bindFactory(AppSessionInjectionFactory.class).to(AppSession.class);
}
}).packages("com.example.bdkosher.restapi");
}
public static void main(String... args) {
SpringApplication.run(CgiWrapplication.class, args);
}
}
Внутри конструктора я связываю AppSession
экземпляры, которые я хочу внедрить, с фабричным классом, который я буду использовать для их внедрения, с креативным именем AppSessionInjectionFactory
.
AppSession
Класс является входной точкой CGI библиотеки взаимодействия приложений / Perl. Это код не имеет отношения к теме, кроме того, что
- Каждый
AppSession
экземпляр привязан к определенному драгоценному идентификатору пользователя String. AppSession
реализуетjava.lang.AutoCloseable
интерфейс. Это позволяет использовать его внутри блоков try-with-resources, а также информировать пользователей API о том, что этот ресурс должен быть закрыт.
Фильтр сервлетов
Я хотел управлять BlockingQueue AppSessionInjectionFactory
исключительно внутри , но столкнулся с проблемами с ограничениями.
А именно, поведение Джерси по умолчанию заключалось в создании нового экземпляра фабрики для каждой инъекции. Я попытался изменить области привязок и изменить общую стратегию привязки на bindFactory(new AppSessionInjectionFactory()).to(AppSession.class)
(обратите внимание, как я создаю отдельный экземпляр, а не предоставляю метод bindFactory с литералом класса). В обоих случаях я не мог заставить Spring ввести AppConfig
боб AppSessionInjectionFactory
. Таким образом, я остановился на использовании фильтра сервлетов, который, как я знал, будет создан только один раз. Вот код фильтра, самая интересная часть приложения, на мой взгляд.
import static com.google.common.base.Preconditions.checkNotNull;
import com.example.bdkosher.restapi.AppConfig;
import com.example.bdkosher.AppSession;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import javax.inject.Singleton;
import javax.servlet.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@WebFilter(description = "Enforces that each HTTP Servlet Request is associated with one and only one session.")
@Component
@Singleton
public class AppSessionFilter implements Filter {
private BlockingQueue<String> sessionUserPool;
@Autowired
private AppConfig config;
@Override
public void init(FilterConfig ignored) throws ServletException {
Set<String> userIds = checkNotNull(checkNotNull(config).getUserIds());
sessionUserPool = new ArrayBlockingQueue<>(userIds.size(), true, userIds);
}
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
String userId = null;
try {
/* This method call will block until a userId is avaialable from the pool. */
userId = sessionUserPool.take();
/* The try-with-resources block ensures the session is closed, even if the filter chain leaves it open. */
try (AppSession session = AppSession.open(userId)) {
req.setAttribute(AppSession.class.getName(), session); // stash the session in the ServletRequest
chain.doFilter(req, res);
}
} catch (InterruptedException ex) {
throw new ServletException("Issue obtaining session user from pool.", ex);
} finally {
/* Return the userId into the pool so future requests can use it. */
if (userId != null) {
sessionUserPool.add(userId);
}
}
}
@Override
public void destroy() {
sessionUserPool.clear();
}
}
Эти doFilter
попытки метод принимать идентификатор пользователя из BlockingQueue. Если нет доступных идентификаторов пользователя, он блокируется до тех пор, пока идентификатор пользователя не будет возвращен в пул. Как только идентификатор пользователя получен, он создает его экземпляр AppSession
и сохраняет его в HttpServletRequest в качестве атрибута для будущего использования другими фильтрами в цепочке. Когда цепочка фильтров возвращается, она заменяет идентификатор пользователя в BlockingQueue.
Фабрика пользовательских инъекций
Последним элементом является сама фабрика, которая делает привязку атрибутов запроса AppSession
доступной любому ресурсу JAX-RS, который в этом нуждается.
Код для этого класса очень прост. Единственная сложная часть — это аннотирование класса, который должен быть обработан по запросу, чтобы он мог вводиться с током HttpServletRequest
при создании экземпляра.
import com.example.bdkosher.AppSession;
import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;
import org.glassfish.hk2.api.Factory;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
@Component
@Scope("request")
public class AppSessionInjectionFactory implements Factory<AppSession> {
private final AppSession session;
@Inject
public RequestBoundWestSessionFactory(HttpServletRequest request) {
Object sessionObj = request.getAttribute(AppSession.class.getName());
if (sessionObj == null) {
throw new IllegalStateException("No Session found to inject. Did you configure the AppSessionFilter correctly?");
}
this.session = (AppSession) sessionObj;
}
@Override
public AppSession provide() {
return session;
}
@Override
public void dispose(AppSession session) {
// intentionally empty
}
}
Выплата
Теперь, когда мы прошли через все эти трудности, чтобы убедиться, что никакие два потока запросов не используют один и тот же экземпляр AppSession, давайте посмотрим на пример ресурса JAX-RS.
import com.example.bdkosher.AppSession;
import javax.ws.rs.*;
import javax.ws.rs.core.Context;
import org.springframework.stereotype.Component;
@Path("/greeting")
@Component
public class HelloWorldResource {
@GET
@Produces("text/plain")
public String greet(@Context AppSession session) {
return "Hello, " + session.getUserId() + "!";
}
}
В реальном приложении я взаимодействую с AppSession
более полезными методами объекта. Но этот простой пример иллюстрирует основную цель: я ввел свой ограниченный ресурс в ресурс JAX-RS, освободив код службы от ответственности за управление им и защиту от общего доступа.