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.