7

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.

  • Since you are working with spring boot, why not deploy each spring data rest app in different tomcat and context(totally independently)? Perhaps you will need to setup a CORS to prevent cross domain. I can't see any problem with this approach. – Thiago Pereira Dec 07 '14 at 01:29
  • I would be interested in that. If you found a solution, please share – Jan Zyka Feb 12 '15 at 09:40

1 Answers1

7

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.

  • Thanks for sharing Daniel...do you by any change have a fully working exampe on github? And btw, did you manage to run all API modules on the same server/process? – dimi Apr 09 '15 at 18:11
  • @dimi, sorry, I don't have anything in a public repo. I could do it if I find the time, but it is basically what I explained before. Apart from that, yes, all API modules are running on the same embedded servlet container. – Daniel Francisco Sabugal Apr 13 '15 at 09:51
  • No problem Daniel. FYI, I uploaded a PoC on [github](https://github.com/dbalaouras/multibaserest) based on your examples (thanks). Seems to work fine and the API is hosted under two base URLs ( **localhost:8080/private/** and **localhost:8080/public/** ), but it is also hosted under the root path: **localhost:8080/localhost/**. Shout if you know how to work around this. ta. – dimi Apr 16 '15 at 00:15
  • 1
    @dimi try excluding `RepositoryRestMvcAutoConfiguration`. As you still have the default DispatcherServlet and your jpa repositories are on the root context because you have declared `@EnableJpaRepositories` on App class, the REST API will be exposed under the root path as well. Also if you want to expose different resources under each API, declare different `@EnableJpaRepositories` annotations on each configuration loaded by the different dispatcher servlets and not on the root context. – Daniel Francisco Sabugal Apr 17 '15 at 10:00
  • Thanks Daniel; excluding `@RepositoryRestMvcAutoConfiguration` did the trick indeed. WRT separate `@EnableJpaRepositories`, it sounds like a great idea, but unfortunately didn't work. It may be my fault, so I'll give it another try during the weekend. Thanks much! – dimi Apr 17 '15 at 11:05