Статьи

JAX-RS 2: пользовательское внедрение @Context ограниченного, небезопасного ресурса

Я работал над созданием веб-приложения 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 вокруг этой библиотеки
  • 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, освободив код службы от ответственности за управление им и защиту от общего доступа.