Springboot 中Jersey 的multiple Application实现

Springboot 2.x 里 jersey 不支持多个Application ,推荐从org.glassfish.jersey.server.ResourceConfig 继承,指定唯一的ApplicationPath. 从下面的一个例子定义来看一下如何定义的

import org.glassfish.jersey.server.ResourceConfig;
import org.springframework.context.annotation.Configuration;

import javax.annotation.PostConstruct;
import javax.inject.Inject;
import javax.inject.Named;
import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Feature;

@Configuration
@ApplicationPath("/samplesvc")
public class SampleResourceConfig extends ResourceConfig {
  @PostConstruct
  public void init() {
    register(SampleResource.class);
  }
}

只有一个Application就意味着只能有一个context,大家只能有一套filter,providers 等等。 如果希望根据不同的path来配置不同类型的filter,provider,resource等的组合,就需要支持多个Application。如何改变Springboot 的行为,先从JerseyAutoConfiguration了解是如何配置的。


/*
 * Copyright 2012-2019 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.boot.autoconfigure.jersey;

import java.util.Collections;
import java.util.EnumSet;

import javax.annotation.PostConstruct;
import javax.servlet.DispatcherType;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRegistration;
import javax.ws.rs.ext.ContextResolver;
import javax.xml.bind.annotation.XmlElement;

import com.fasterxml.jackson.databind.AnnotationIntrospector;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.cfg.MapperConfig;
import com.fasterxml.jackson.module.jaxb.JaxbAnnotationIntrospector;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.glassfish.jersey.jackson.JacksonFeature;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.server.spring.SpringComponentProvider;
import org.glassfish.jersey.servlet.ServletContainer;
import org.glassfish.jersey.servlet.ServletProperties;

import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.AutoConfigureOrder;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.ConditionalOnMissingFilterBean;
import org.springframework.boot.autoconfigure.web.servlet.DefaultJerseyApplicationPath;
import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.JerseyApplicationPath;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.DynamicRegistrationBean;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.util.ClassUtils;
import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.context.ServletContextAware;
import org.springframework.web.filter.RequestContextFilter;

/**
 * {@link EnableAutoConfiguration Auto-configuration} for Jersey.
 *
 * @author Dave Syer
 * @author Andy Wilkinson
 * @author Eddú Meléndez
 * @author Stephane Nicoll
 * @since 1.2.0
 */
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ SpringComponentProvider.class, ServletRegistration.class })
@ConditionalOnBean(type = "org.glassfish.jersey.server.ResourceConfig")
@ConditionalOnWebApplication(type = Type.SERVLET)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@AutoConfigureBefore(DispatcherServletAutoConfiguration.class)
@AutoConfigureAfter(JacksonAutoConfiguration.class)
@EnableConfigurationProperties(JerseyProperties.class)
public class JerseyAutoConfiguration implements ServletContextAware {

    private static final Log logger = LogFactory.getLog(JerseyAutoConfiguration.class);

    private final JerseyProperties jersey;

    private final ResourceConfig config;

    private final ObjectProvider customizers;

    public JerseyAutoConfiguration(JerseyProperties jersey, ResourceConfig config,
            ObjectProvider customizers) {
        this.jersey = jersey;
        this.config = config;
        this.customizers = customizers;
    }

    @PostConstruct
    public void path() {
        customize();
    }

    private void customize() {
        this.customizers.orderedStream().forEach((customizer) -> customizer.customize(this.config));
    }

    @Bean
    @ConditionalOnMissingFilterBean(RequestContextFilter.class)
    public FilterRegistrationBean requestContextFilter() {
        FilterRegistrationBean registration = new FilterRegistrationBean<>();
        registration.setFilter(new RequestContextFilter());
        registration.setOrder(this.jersey.getFilter().getOrder() - 1);
        registration.setName("requestContextFilter");
        return registration;
    }

    @Bean
    @ConditionalOnMissingBean
    public JerseyApplicationPath jerseyApplicationPath() {
        return new DefaultJerseyApplicationPath(this.jersey.getApplicationPath(), this.config);
    }

    @Bean
    @ConditionalOnMissingBean(name = "jerseyFilterRegistration")
    @ConditionalOnProperty(prefix = "spring.jersey", name = "type", havingValue = "filter")
    public FilterRegistrationBean jerseyFilterRegistration(JerseyApplicationPath applicationPath) {
        FilterRegistrationBean registration = new FilterRegistrationBean<>();
        registration.setFilter(new ServletContainer(this.config));
        registration.setUrlPatterns(Collections.singletonList(applicationPath.getUrlMapping()));
        registration.setOrder(this.jersey.getFilter().getOrder());
        registration.addInitParameter(ServletProperties.FILTER_CONTEXT_PATH, stripPattern(applicationPath.getPath()));
        addInitParameters(registration);
        registration.setName("jerseyFilter");
        registration.setDispatcherTypes(EnumSet.allOf(DispatcherType.class));
        return registration;
    }

    private String stripPattern(String path) {
        if (path.endsWith("/*")) {
            path = path.substring(0, path.lastIndexOf("/*"));
        }
        return path;
    }

    @Bean
    @ConditionalOnMissingBean(name = "jerseyServletRegistration")
    @ConditionalOnProperty(prefix = "spring.jersey", name = "type", havingValue = "servlet", matchIfMissing = true)
    public ServletRegistrationBean jerseyServletRegistration(JerseyApplicationPath applicationPath) {
        ServletRegistrationBean registration = new ServletRegistrationBean<>(
                new ServletContainer(this.config), applicationPath.getUrlMapping());
        addInitParameters(registration);
        registration.setName(getServletRegistrationName());
        registration.setLoadOnStartup(this.jersey.getServlet().getLoadOnStartup());
        return registration;
    }

    private String getServletRegistrationName() {
        return ClassUtils.getUserClass(this.config.getClass()).getName();
    }

    private void addInitParameters(DynamicRegistrationBean registration) {
        this.jersey.getInit().forEach(registration::addInitParameter);
    }

    @Override
    public void setServletContext(ServletContext servletContext) {
        String servletRegistrationName = getServletRegistrationName();
        ServletRegistration registration = servletContext.getServletRegistration(servletRegistrationName);
        if (registration != null) {
            if (logger.isInfoEnabled()) {
                logger.info("Configuring existing registration for Jersey servlet '" + servletRegistrationName + "'");
            }
            registration.setInitParameters(this.jersey.getInit());
        }
    }

    @Order(Ordered.HIGHEST_PRECEDENCE)
    public static final class JerseyWebApplicationInitializer implements WebApplicationInitializer {

        @Override
        public void onStartup(ServletContext servletContext) throws ServletException {
            // We need to switch *off* the Jersey WebApplicationInitializer because it
            // will try and register a ContextLoaderListener which we don't need
            servletContext.setInitParameter("contextConfigLocation", "");
        }

    }

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnClass(JacksonFeature.class)
    @ConditionalOnSingleCandidate(ObjectMapper.class)
    static class JacksonResourceConfigCustomizer {

        @Bean
        ResourceConfigCustomizer resourceConfigCustomizer(final ObjectMapper objectMapper) {
            return (ResourceConfig config) -> {
                config.register(JacksonFeature.class);
                config.register(new ObjectMapperContextResolver(objectMapper), ContextResolver.class);
            };
        }

        @Configuration(proxyBeanMethods = false)
        @ConditionalOnClass({ JaxbAnnotationIntrospector.class, XmlElement.class })
        static class JaxbObjectMapperCustomizer {

            @Autowired
            void addJaxbAnnotationIntrospector(ObjectMapper objectMapper) {
                JaxbAnnotationIntrospector jaxbAnnotationIntrospector = new JaxbAnnotationIntrospector(
                        objectMapper.getTypeFactory());
                objectMapper.setAnnotationIntrospectors(
                        createPair(objectMapper.getSerializationConfig(), jaxbAnnotationIntrospector),
                        createPair(objectMapper.getDeserializationConfig(), jaxbAnnotationIntrospector));
            }

            private AnnotationIntrospector createPair(MapperConfig config,
                    JaxbAnnotationIntrospector jaxbAnnotationIntrospector) {
                return AnnotationIntrospector.pair(config.getAnnotationIntrospector(), jaxbAnnotationIntrospector);
            }

        }

        private static final class ObjectMapperContextResolver implements ContextResolver {

            private final ObjectMapper objectMapper;

            private ObjectMapperContextResolver(ObjectMapper objectMapper) {
                this.objectMapper = objectMapper;
            }

            @Override
            public ObjectMapper getContext(Class type) {
                return this.objectMapper;
            }

        }

    }

}

在Springboot 原生的Autoconfig 中,可以看到他基本做了如下几件事情

  • 注册 jersey ServletContainer同时把application path 作为此servlet的 url mapping
ServletRegistrationBean registration = new ServletRegistrationBean<>(
                new ServletContainer(this.config), applicationPath.getUrlMapping());
  • 用customizers 进行配置。实现了ResourceConfigCustomizer

从JerseyAutoConfiguration的实现可以看出,如果能再写另外一个Autoconfig,可以读多个application path , 就可以支持多个application了。例如,再写一个Autoconfig 命名成MultipleApplicationJerseyAutoConfiguration 。在这样的情况下,出现了两个AutoConfiguration来处理ApplicationPath, 如何避免冲突? 根据JerseyAutoConfiguration的定义, 只要不出现org.glassfish.jersey.server.ResourceConfig的bean就行了。

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ SpringComponentProvider.class, ServletRegistration.class })
@ConditionalOnBean(type = "org.glassfish.jersey.server.ResourceConfig")
@ConditionalOnWebApplication(type = Type.SERVLET)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@AutoConfigureBefore(DispatcherServletAutoConfiguration.class)
@AutoConfigureAfter(JacksonAutoConfiguration.class)
@EnableConfigurationProperties(JerseyProperties.class)
public class JerseyAutoConfiguration implements ServletContextAware {
}

那么可以有如下实现

@Configuration
@ConditionalOnClass(name = { "org.glassfish.jersey.server.spring.SpringComponentProvider",
    "javax.servlet.ServletRegistration" })
@ConditionalOnWebApplication
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 1)
@AutoConfigureBefore(DispatcherServletAutoConfiguration.class)
@EnableConfigurationProperties(JerseyProperties.class)
public class MultipleApplicationJerseyAutoConfiguration implements ServletContextInitializer {

  private static final Logger logger = LoggerFactory.getLogger(GingerServerAutoConfig.class);

  private final JerseyProperties jersey;

  private final List jaxAppList;

  private final ObjectProvider customizers;

  public MultipleApplicationJerseyAutoConfiguration(JerseyProperties jersey, List config,
                                ObjectProvider customizers) {
    if (config == null || config.isEmpty()) {
      throw new IllegalArgumentException("Application can't be null!");
    }

    this.jersey = jersey;
    this.jaxAppList = config;
    this.customizers = customizers;
  }

  protected String findApplicationPath(ApplicationPath annotation) {
    // Jersey doesn't like to be the default servlet, so map to /* as a fallback
    if (annotation == null) {
      return null;
    }

    String applicationPath = annotation.value();
    if (!applicationPath.startsWith("/")) {
      applicationPath = "/" + applicationPath;
    }

    if (!applicationPath.endsWith("/*")) {
      if (applicationPath.endsWith("/")) {
        applicationPath = applicationPath + "*";
      } else {
        applicationPath = applicationPath + "/*";
      }
    }

    return applicationPath;
  }

  @Override
  public void onStartup(ServletContext servletContext) throws ServletException {
    setServletContext(servletContext);
  }

  protected void registerServletMapping(Application jaxrsApp, Dynamic servletReg) {
    String mappingPath = findApplicationPath(AnnotationUtils.findAnnotation(jaxrsApp.getClass(), ApplicationPath.class));
    if (mappingPath != null) {
      if (logger.isInfoEnabled()) {
        logger.info("Adding servlet with Path: " + mappingPath);
      }
      servletReg.addMapping(mappingPath);
    }
  }

  @Bean
  @ConditionalOnMissingBean
  public FilterRegistrationBean gingerServerRequestContextFilter() {
    FilterRegistrationBean registration = new FilterRegistrationBean();
    registration.setFilter(new RequestContextFilter());
    registration.setOrder(this.jersey.getFilter().getOrder() - 1);
    registration.setName("requestContextFilter");
    return registration;
  }

  private void setServletContext(ServletContext servletContext) {
    for (Application jaxrsApp : jaxAppList) {
      final ResourceConfig resourceConfig = ResourceConfig.forApplication(jaxrsApp);
      customizers.orderedStream().forEach((customizer) -> customizer.customize(resourceConfig));
      Dynamic servletReg = servletContext.addServlet(ClassUtils.getUserClass(jaxrsApp.getClass()).getName(),
          new ServletContainer(resourceConfig));
      if (servletReg != null) {
        servletReg.setInitParameters(jersey.getInit());
        servletReg.setInitParameter(CommonProperties.METAINF_SERVICES_LOOKUP_DISABLE, Boolean.TRUE.toString());
        servletReg.setAsyncSupported(true);

        registerServletMapping(jaxrsApp, servletReg);
      }
    }
  }

}

  • 实现了在tomcatStarter 启动的时候被调用
class TomcatStarter implements ServletContainerInitializer {

    private static final Log logger = LogFactory.getLog(TomcatStarter.class);

    private final ServletContextInitializer[] initializers;

    private volatile Exception startUpException;

    TomcatStarter(ServletContextInitializer[] initializers) {
        this.initializers = initializers;
    }

    @Override
    public void onStartup(Set> classes, ServletContext servletContext) throws ServletException {
        try {
            for (ServletContextInitializer initializer : this.initializers) {
                initializer.onStartup(servletContext);
            }
        }
        catch (Exception ex) {
            this.startUpException = ex;
            // Prevent Tomcat from logging and re-throwing when we know we can
            // deal with it in the main thread, but log for information here.
            if (logger.isErrorEnabled()) {
                logger.error("Error starting Tomcat context. Exception: " + ex.getClass().getName() + ". Message: "
                        + ex.getMessage());
            }
        }
    }

    Exception getStartUpException() {
        return this.startUpException;
    }

}


顺便提一下tomat 启动的调用链为

AbstractApplicationContext.onRefresh ->ServletWebServerApplicationContext.onRefresh->ServletWebServerApplicationContext.createWebServer->TomcatServletWebServerFactory.getWebServer->TomcatServletWebServerFactory.prepareContext 

最后把这些initializer 注入进tomcat
TomcatStarter starter = new TomcatStarter(initializers);
  • 实现了扫描多个appliction,来注册多个jersey servlet
  private void setServletContext(ServletContext servletContext) {
    for (Application jaxrsApp : jaxAppList) {
      final ResourceConfig resourceConfig = ResourceConfig.forApplication(jaxrsApp);
      customizers.orderedStream().forEach((customizer) -> customizer.customize(resourceConfig));
      Dynamic servletReg = servletContext.addServlet(ClassUtils.getUserClass(jaxrsApp.getClass()).getName(),
          new ServletContainer(resourceConfig));
      if (servletReg != null) {
        servletReg.setInitParameters(jersey.getInit());
        servletReg.setInitParameter(CommonProperties.METAINF_SERVICES_LOOKUP_DISABLE, Boolean.TRUE.toString());
        servletReg.setAsyncSupported(true);

        registerServletMapping(jaxrsApp, servletReg);
      }
    }
  }

}

根据上文的分析,application的注册就不能从org.glassfish.jersey.server.ResourceConfig来继承,而是从Application来继承(防止JerseyAutoConfiguration 也启动)下面是一个例子

@Configuration
@ApplicationPath("/samplesvc/v1")
public class SampleResourceConfig extends Application {
    @Override
    public Set> getClasses() {
        Set> providers = new LinkedHashSet>();
        providers.add(SampleResource1.class);
        return providers;
    }

}

@Configuration
@ApplicationPath("/samplesvc/v2")
public class SampleResourceConfig2 extends Application {
    @Override
    public Set> getClasses() {
        Set> providers = new LinkedHashSet>();
        providers.add(SampleResource2.class);
        return providers;
    }

}

你可能感兴趣的:(Springboot 中Jersey 的multiple Application实现)