具有两种 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的主要内容,如果未能解决你的问题,请参考以下文章