使用两个MVC配置的Spring Boot

25

我有一个使用Jackson进行JSON视图配置的Spring Boot应用程序,其中包含REST API。这很好用,我能够获得所有Spring Boot的优点。

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

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

到目前为止,我所能接近的是使用两个调度程序Servlet,每个Servlet都有自己的MVC配置,但这会导致Boot放弃了许多我自动获取的东西,基本上也就失去了使用boot的原因。

我不能将应用程序分解成多个应用程序。

"您无法在Boot中执行此操作并仍然获得其全部功能"是可以接受的答案。虽然似乎它应该能够处理这个问题。


你尝试过了吗?这篇文章讨论了如何使用Spring Boot的多个Dispatcher Servlet和Web Context。 - Rozart
1
请参考以下链接:https://dev59.com/cmEi5IYBdhLWcg3wHJAR - atamanroman
两个调度程序似乎是简单的解决方案。您仍然可以从自动配置类中获得灵感/复制。但没有真正的魔法。 有什么阻止您在Tomcat上部署2个应用程序吗?您可以将应用程序之间共享的类放在您自己的库中,这两个项目都作为依赖项。尽管这需要2个上下文,并且在单个服务器上加载bean两次。您还可以放弃lib,而是通过REST使两个应用程序通信。 - Schaka
2
你会失去哪种具体的魔力? - Rozart
1
仅为完整起见。内容协商不是一个好方法吗?您可以为不同(自定义)媒体类型注册消息转换器。 - Hendrik Jander
显示剩余4条评论
4个回答

4
有几种方法可以实现此目标。根据您的要求,我认为这是管理REST API版本的情况。
有多种方式来为REST API添加版本号,其中一些流行的方法包括基于URL的版本控制和其他在评论中提到的技术。
基于URL的方法更多地关注于具有多个地址版本:
例如: 对于V1:
/path/v1/resource

和 V2 :

/path/v2/resource

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

解决API版本的另一种选项是使用headers,这样只有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 Mapper,即不同的JSON mapper配置,则无论Spring上下文如何,您都需要一种不同的序列化/反序列化策略。

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

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

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

通过结合Rest版本控制和自定义序列化策略,可以在不必连接多个分发器Servlet配置的情况下管理每个API的上下文。


每个API的MVC配置差异还有一些细微之处,不仅仅是JSON格式。但我认为这个答案对于我所提供的信息量来说是有意义的,并且这也是我在这个问题和其他问题上收到的唯一真正的答案。你获得了赏金;-) 谢谢,我将使用你的答案精神解决我问题的其他方面。 - SingleShot

4

针对我昨天的评论和@Ashoka Header的想法,我建议为自定义媒体类型注册2个消息转换器(传统和当前)。您可以像这样实现:

@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 提出的版本头重写为自定义媒体类型,方法如下:

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");
            }
       .....
    }
}

这可能不是你想要的确切答案,但也许可以提供一些启发。拦截器可以像这样注册。


这确实给了我一些想法。我实际上无法更改客户端以添加标头等,但是我可以拦截并查看基本URL,然后自己添加标头或类似内容。感谢您的建议。 - SingleShot

2

如果你可以接受每个上下文使用不同的端口,那么你只需要覆盖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中,你可以使用不同的配置文件(如devtest)。
启动应用程序时,使用-Dspring.profiles.active=dev-Dspring.profiles.active=test参数,并在properties目录下使用名为application-dev.propertiesapplication-test.properties的不同属性文件。 这可能会解决问题。

1
谢谢您的回答。不幸的是,它与我正在尝试解决的问题无关。 - SingleShot

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接