具有两种 MVC 配置的 Spring Boot

Posted

技术标签:

【中文标题】具有两种 MVC 配置的 Spring Boot【英文标题】:Spring Boot with Two MVC Configurations 【发布时间】:2016-04-16 04:35:59 【问题描述】:

我有一个带有 REST API 的 Spring Boot 应用程序,使用 Jackson 进行 JSON 视图配置。它工作得很好,我可以获得 Spring Boot 的所有优点。

但是,我需要添加一个类似但具有不同设置的附加 REST API。例如,除其他外,它需要不同的 Jackson 对象映射器配置,因为 JSON 看起来会有很大不同(例如,没有 JSON 数组)。这只是一个例子,但有很多不同之处。每个 API 都有不同的上下文(例如 /api/current 和 /api/legacy)。

理想情况下,我希望将两个 MVC 配置映射到这些不同的上下文,并且不必放弃启动时的任何自动连接。

到目前为止,我所能接近的只是使用两个调度程序 servlet,每个 servlet 都有自己的 MVC 配置,但这会导致 Boot 丢弃一大堆我自动获得的东西,并且基本上破坏了使用 boot 的原因。

我无法将应用拆分为多个应用。

“你不能用 Boot 做到这一点并仍然获得它的所有魔力”的答案是一个可以接受的答案。看来它应该能够处理这个问题。

【问题讨论】:

***.com/questions/29096511/… - 你试过了吗? 也在这里:***.com/questions/21630820/… 澄清一下:他们需要生活在同一个父母环境中吗?所以共享相同的豆子? 具体会失去什么样的魔法? 只是为了完整性。内容协商会不会是一种巧妙的方法?您可以为不同的(自定义)媒体类型注册消息转换器。 【参考方案1】:

有几种方法可以实现这一目标。根据您的要求,我说这是管理 REST API 版本的案例。 有几种方法可以对 REST API 进行版本控制,其中一些流行的是版本 url 和 cmets 链接中提到的其他技术。 基于 URL 的方法更倾向于拥有多个版本的地址:

例如 对于 V1

/path/v1/resource

和 V2

/path/v2/resource

这些将解析为 Spring MVC Controller bean 中的 2 个不同方法,调用被委托给这些方法。

解决API版本的另一个选项是使用标头,这种方式只有URL,基于版本的多种方法。 例如:

/path/resource

标题:

X-API-Version: 1.0

标题:

X-API-Version: 2.0

这也将在控制器上的两个单独操作中解决。

现在这些是可以处理多个 rest 版本的策略。

以下方法很好地解释了上述方法:git example

注意:以上是一个spring boot的应用。

这两种方法的共同点是需要不同的 POJOS,基于哪个 Jackson JSON 库自动将指定类型的实例编组为 JSON。

即假设代码使用@RestController [org.springframework.web.bind.annotation.RestController]

现在,如果您的要求是具有不同的 JSON 映射器,即不同的 JSON 映射器配置,那么无论 Spring 上下文如何,您都需要不同的序列化/反序列化策略。

在这种情况下,您需要实现自定义反序列化器 CustomDeSerializer,它将扩展 JsonDeserializer<T> [com.fasterxml.jackson.databind.JsonDeserializer] 并在 deserialize() 中实现您的自定义 startegy。

在目标 POJO 上使用@JsonDeserialize(using = CustomDeSerializer.class) 注解。

这样可以使用不同的反序列化器管理多个 JSON 方案。

通过结合 Rest 版本控制 + 自定义序列化策略,每个 API 都可以在其自己的上下文中进行管理,而无需连接多个调度程序 Servlet 配置。

【讨论】:

除了我的意思是 JSON 格式之外,每个 API 的 MVC 配置还有一些差异,但我认为这个答案对于我给你的信息量是有意义的,并且是唯一真正的答案我收到了这个问题和我问过的其他问题。你得到了赏金 ;-) 谢谢,我会用你回答的精神来解决我的问题的其他方面。【参考方案2】:

扩展我昨天的评论和@Ashoka Header 的想法,我建议为自定义媒体类型注册 2 个 MessageConverters(旧版和当前)。你可以这样做:

@Bean
MappingJackson2HttpMessageConverter currentMappingJackson2HttpMessageConverter() 
    MappingJackson2HttpMessageConverter jsonConverter = new MappingJackson2HttpMessageConverter();
    ObjectMapper objectMapper = new ObjectMapper();
    // set features
    jsonConverter.setObjectMapper(objectMapper);

    jsonConverter.setSupportedMediaTypes(Arrays.asList(new MediaType("json", "v2")));

    return jsonConverter;



@Bean
MappingJackson2HttpMessageConverter legacyMappingJackson2HttpMessageConverter() 
    MappingJackson2HttpMessageConverter jsonConverter = new MappingJackson2HttpMessageConverter();
    ObjectMapper objectMapper = new ObjectMapper();
    // set features
    jsonConverter.setObjectMapper(objectMapper);
    return jsonConverter;

注意其中一个转换器的自定义媒体类型。

如果您愿意,可以使用拦截器将@Ashoka 提出的 Version-Headers 重写为自定义的 Media-Type,如下所示:

public class ApiVersionMediaTypeMappingInterceptor extends HandlerInterceptorAdapter 
    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response, Object handler) throws Exception 
        try 
            if(request.getHeader("X-API-Version") == "2") 
                request.setAttribute("Accept:","json/v2");
            
       .....
    

这可能不是您要寻找的确切答案,但也许它可以提供一些灵感。一个拦截器注册了like so。

【讨论】:

这确实给了我一些想法。我实际上无法更改客户端以添加标头等,但是我可以拦截并查看基本 URL,然后自己添加标头或类似内容。谢谢你的想法。【参考方案3】:

如果您可以为每个上下文使用不同的端口,那么您只需覆盖 DispatcherServletAutoConfiguration bean。所有其余的魔法作品,multpart,Jackson 等。您可以为每个子上下文分别配置 Servlet 和 Jackson/Multipart 等,并注入父上下文的 bean。

package test;

import static org.springframework.boot.autoconfigure.web.DispatcherServletAutoConfiguration.DEFAULT_DISPATCHER_SERVLET_BEAN_NAME;
import static org.springframework.boot.autoconfigure.web.DispatcherServletAutoConfiguration.DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.context.embedded.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

@Configuration
@EnableAutoConfiguration(exclude = 
        Application.Context1.class,
        Application.Context2.class
)
public class Application extends WebMvcConfigurerAdapter 

    @Bean
    public TestBean testBean() 
        return new TestBean();
    

    public static void main(String[] args) 
        final SpringApplicationBuilder builder = new SpringApplicationBuilder().parent(Application.class);
        builder.child(Context1.class).run();
        builder.child(Context2.class).run();
    

    public static class TestBean 
    

    @Configuration
    @EnableAutoConfiguration(exclude = Application.class, Context2.class)
    @PropertySource("classpath:context1.properties")
    public static class Context1 

        @Bean(name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
        DispatcherServlet dispatcherServlet() 
            DispatcherServlet dispatcherServlet = new DispatcherServlet();
            // custom config here
            return dispatcherServlet;
        

        @Bean(name = DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME)
        ServletRegistrationBean dispatcherServletRegistration() 
            ServletRegistrationBean registration = new ServletRegistrationBean(dispatcherServlet(), "/test1");
            registration.setName(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME);
            // custom config here
            return registration;
        

        @Bean
        Jackson2ObjectMapperBuilder jackson2ObjectMapperBuilder(TestBean testBean) 
            System.out.println(testBean);
            Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
            // custom config here
            return builder;
        
    

    @Configuration
    @EnableAutoConfiguration(exclude = Application.class, Context1.class)
    @PropertySource("classpath:context2.properties")
    public static class Context2 

        @Bean(name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
        DispatcherServlet dispatcherServlet() 
            DispatcherServlet dispatcherServlet = new DispatcherServlet();
            // custom config here
            return dispatcherServlet;
        

        @Bean(name = DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME)
        ServletRegistrationBean dispatcherServletRegistration() 
            ServletRegistrationBean registration = new ServletRegistrationBean(dispatcherServlet(), "/test2");
            registration.setName(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME);
            // custom config here
            return registration;
        

        @Bean
        Jackson2ObjectMapperBuilder jackson2ObjectMapperBuilder(TestBean testBean) 
            System.out.println(testBean);
            Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
            // custom config here
            return builder;
        
    

context1/2.properties 文件目前仅包含一个 server.port=8080/8081,但您可以在那里为子上下文设置所有其他 spring 属性。

【讨论】:

【参考方案4】:

在 Spring-boot 中,ypu 可以使用不同的配置文件(例如 devtest)。

启动应用程序 -Dspring.profiles.active=dev-Dspring.profiles.active=test 并在您的properties 目录中使用名为application-dev.propertiesapplication-test.properties 的不同属性文件。 这可以解决问题。

【讨论】:

感谢您的回答。不幸的是,这与我要解决的问题无关。

以上是关于具有两种 MVC 配置的 Spring Boot的主要内容,如果未能解决你的问题,请参考以下文章

使用Spring AOP 记录Spring MVC请求日志

使用Spring AOP 记录Spring MVC请求日志

使用Spring AOP 记录Spring MVC请求日志

使用Spring AOP 记录Spring MVC请求日志

Spring MVC总结1

具有 Postgresql 部署的 Spring Boot MVC 应用程序在 Heroku 中失败