Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring Boot (JAR) with multiple dispatcher servlets for different REST APIs with Spring Data REST

I have a project that uses Spring Boot to generate an executable JAR that exposes a REST API with Spring Data REST. It is also integrated with Spring Security OAuth. That works fine. My problem is the following,

I want to have different modules for the REST API that I want to enable only if the correspondent JAR with the JPA repositories is in the classpath (it has been defined as a dependency).

The thing is I want them to be independent from each other. I want to be able to serve them under different dispatcher servlets with different mappings so I can specify different baseUri for each one and have different root URLs for the resources discovery.

I´m gonna try to make it more clear:

  • Module A of API:

    • A JAR containing for example XRespository and YRespository for resources X and Y.
    • Dispatcher servlet A.
    • Servlet mapping: /api/moduleA/
    • Base URI for Spring Data REST: /api/moduleA/
    • If I check URL /api/moduleA/ I should discover resources X and Y.
  • Module B of API:

    • A JAR containing for example PRespository and QRespository for resources P and Q.
    • Dispatcher servlet B.
    • Servlet mapping: /api/moduleB/
    • Base URI for Spring Data REST: /api/moduleB/
    • If I check URL /api/moduleB/ I should discover resources P and Q.
  • More modules...

Apart from that I can have another dispatcher servlet where I hold /oauth/* endpoints along with other custom controllers, and the security configuration has to work properly for all (/*)

I know I can define more dispatcher servlets through ServletRegistrationBean but I don´t know how to attach to each one different spring data rest configurations.

I´ve also been trying to do this with hierarchical application contexts with SpringApplicationBuilder by having in each child context the configuration that defines each dispatcher servlet, each RepositoryRestMvcConfiguration and having each @EnableJpaRepositories annotation defining different packages to scan. Anyway I can´t not even load the context as they are not created as WebApplicationContext thus failing because there is no ServletContext available.

Any help/suggestion? Thanks in advance.

like image 925
Daniel Francisco Sabugal Avatar asked Dec 05 '14 15:12

Daniel Francisco Sabugal


People also ask

Can we have more than one dispatcher servlet in spring?

You can have as many DispatcherServlets as you want. Basically what you need to do is duplicate the configuration and give the servlet a different name (else it will overwrite the previous one), and have some separate configuration classes (or xml files) for it.

Why do we need multiple dispatcher servlet in spring?

Strictly seperating user and admin functionality. Or one for plain Spring MVC and another for Spring Web Flow. If there are major configuration differences for some controllers. (We actually used the Spring MVC and Spring Web Flow seperation so that we could add this without affecting the already exising configs).

Can we use more than one DispatcherServlet?

Yes, you can have any number of DispatcherServlets.

How do I get all API endpoints in spring boot?

Mapping Endpoints In a Spring Boot application, we expose a REST API endpoint by using the @RequestMapping annotation in the controller class. For getting these endpoints, there are three options: an event listener, Spring Boot Actuator, or the Swagger library.


1 Answers

I found the solution a while ago but I forgot to share it here, so thanks Jan for reminding me that.

I solved it by creating and registering several dispatchers servlets with new web application contexts with different configurations (RepositoryRestMvcConfiguration) and a common parent which is the root application context of the Spring Boot application. To enable the API modules automatically depending on the different jars included on the classpath I emulated what Spring Boot does more or less.

The project is divided in several gradle modules. Something like this:

  • project-server
  • project-api-autoconfigure
  • project-module-a-api
  • project-module-b-api
  • ...
  • project-module-n-api

The module project-server is the main one. It declares a dependency on project-api-autoconfigure, and at the same time it excludes the transitive dependencies of project-api-autoconfigure on project-module-?-api module(s).

Inside project-server.gradle:

dependencies {
    compile (project(':project-api-autoconfigure')) {
        exclude module: 'project-module-a-api'
        exclude module: 'project-module-b-api'
        ...
    }
    ...
}

project-api-autoconfigure depends on all the API modules, so the dependencies will look like this on project-api-autoconfigure.gradle:

dependencies {
    compile project(':project-module-a-api')
    compile project(':project-module-b-api')
    ...
}

project-api-autoconfigure is where I create the dispatcher servlet beans with their own web application context for every API module, but this configurations are conditional on the configuration classes of every API module which live inside each API module jar.

I created and abstract class from which every autoconfiguration class inherit:

public abstract class AbstractApiModuleAutoConfiguration<T> {

    @Autowired
    protected ApplicationContext applicationContext;

    @Autowired
    protected ServerProperties server;

    @Autowired(required = false)
    protected MultipartConfigElement multipartConfig;

    @Value("${project.rest.base-api-path}")
    protected String baseApiPath;

    protected DispatcherServlet createApiModuleDispatcherServlet() {
        AnnotationConfigWebApplicationContext webContext = new AnnotationConfigWebApplicationContext();
        webContext.setParent(applicationContext);
        webContext.register(getApiModuleConfigurationClass());
        return new DispatcherServlet(webContext);
    }

    protected ServletRegistrationBean createApiModuleDispatcherServletRegistration(DispatcherServlet apiModuleDispatcherServlet) {
        ServletRegistrationBean registration = new ServletRegistrationBean(
                apiModuleDispatcherServlet,
                this.server.getServletMapping() + baseApiPath + "/" + getApiModulePath() + "/*");

        registration.setName(getApiModuleDispatcherServletBeanName());
        if (this.multipartConfig != null) {
            registration.setMultipartConfig(this.multipartConfig);
        }
        return registration;
    }

    protected abstract String getApiModuleDispatcherServletBeanName();

    protected abstract String getApiModulePath();

    protected abstract Class<T> getApiModuleConfigurationClass();

}

So now, the autoconfiguration class for module A will look something like this:

@Configuration
@ConditionalOnClass(ApiModuleAConfiguration.class)
@ConditionalOnProperty(prefix = "project.moduleA.", value = "enabled")
public class ApiModuleAAutoConfiguration extends AbstractApiModuleAutoConfiguration<ApiModuleAConfiguration> {

    public static final String API_MODULE_A_DISPATCHER_SERVLET_BEAN_NAME = "apiModuleADispatcherServlet";
    public static final String API_MODULE_A_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME = "apiModuleADispatcherServletRegistration";

    @Value("${project.moduleA.path}")
    private String apiModuleAPath;

    @Bean(name = API_MODULE_A_DISPATCHER_SERVLET_BEAN_NAME)
    public DispatcherServlet apiModuleADispatcherServlet() {
        return createApiModuleDispatcherServlet();
    }

    @Bean(name = API_MODULE_A_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME)
    public ServletRegistrationBean apiModuleADispatcherServletRegistration() {
        return createApiModuleDispatcherServletRegistration(apiModuleADispatcherServlet());
    }

    @Override
    protected String getApiModuleDispatcherServletBeanName() {
        return API_MODULE_A_DISPATCHER_SERVLET_BEAN_NAME;
    }

    @Override
    protected String getApiModulePath() {
        return apiModuleAPath;
    }

    @Override
    protected Class<ApiModuleAConfiguration> getApiModuleConfigurationClass() {
        return ApiModuleAConfiguration.class;
    }

}

And now, your ApiModuleAConfiguration, ApiModuleBConfiguration... configuration classes will be on each api module project-module-a-api, project-module-b-api...

They can be RepositoryRestMvcConfiguration or they can extend from it or they can be any other configuration class that imports the Spring Data REST configuration.

And last but not least, I created different gradle scripts inside the main module project-server to be loaded based on a property passed to gradle to emulate Maven profiles. Each script declares as dependencies the api modules that need to be included. It looks something like this:

- project-server
    /profiles/
        profile-X.gradle
        profile-Y.gradle
        profile-Z.gradle

and for example, profile-X enables API modules A and B:

dependencies {
    compile project(':project-module-a-api')
    compile project(':project-module-b-api')
}

processResources {
    from 'src/main/resources/profiles/profile-X'
    include 'profile-x.properties'
    into 'build/resources/main'
}

Other profiles could enable different API modules.

Profiles are loaded this way from the project-server.gradle:

loadProfile()

processResources {
    include '**/*'
    exclude 'profiles'
}

dependencies {
        compile (project(':project-api-autoconfigure')) {
            exclude module: 'project-module-a-api'
            exclude module: 'project-module-b-api'
            ...
        }
        ...
    }

...

def loadProfile() {
    def profile = hasProperty('profile') ? "${profile}" : "dev"
    println "Profile: " + profile
    apply from: "profiles/" + profile + ".gradle"
}

And that's all more or less. I hope it helps you Jan.

Cheers.

like image 66
Daniel Francisco Sabugal Avatar answered Dec 25 '22 02:12

Daniel Francisco Sabugal