Issue
Is it possible to Define a Spring RestController (@RestController
annotated class) solely in the Java Configuration (the class with @Configuration
annotated in the method marked with @Bean
)?
I have an application managed by spring boot (the version doesn't matter for the sake of the question, even the last one available). This application exposes some endpoints with REST, so there are several rest controllers, which in turn call the services (as usual).
Now depending on configuration (property in application.yml
) I would like to avoid starting some services and, say 2 classes annotated with @RestController
annotation because they deal with the "feature X" that I want to exclude.
I would like to configure all my beans via Java configuration, and this is a requirement. So my initial approach was to define all the beans (controllers and services) in a separate configuration which is found by spring boot during the scanning) and put a @ConditionalOnProperty
on the configuration so that it will appear in one place:
@Configuration
public class MyAppGeneralConfiguration {
// here I define all the beans that are not relevant for "feature X"
@Bean
public ServiceA serviceA() {}
...
}
@Configuration
@ConditionalOnProperty(name = "myapp.featureX.enabled", havingValue = "true")
public class MyAppFeatureXConfiguration {
// here I will define all the beans relevant for feature X:
@Bean
public ServiceForFeatureX1 serviceForFeatureX1() {}
@Bean
public ServiceForFeatureX2 serviceForFeatureX2() {}
}
With this approach My services do not have any spring annotations at all and I don't use @Autowired
annotation as everything is injected via the constructors in @Configuration
class:
// no @Service / @Component annotation
public class ServiceForFeatureX1 {}
Now my question is about the classes annotated with @RestContoller
annotation. Say I have 2 Controllers like this:
@RestController
public class FeatureXRestController1 {
...
}
@RestController
public class FeatureXRestController2 {
...
}
Ideally I would like to define them in the Java Configuration as well, so that these two controllers won't even load when I disable the feature:
@ConditionalOnProperty(name = "myapp.featureX.enabled", havingValue = "true", matchIfMissing=true)
public class MyAppFeatureXConfiguration {
@Bean
@RestController // this doesn't work because the @RestController has target Type and can't be applied
// to methods
public FeatureXRestController1 featureXRestController1() {
}
So the question is basically is it possible to do that?
RestController is a Controller which is in turn a component hence its subject to component scanning. Hence if the feature X is disabled the rest controllers for feature X will still start loading and fail because there won't be no "services" - beans excluded in the configuration, so spring boot won't be able to inject.
One way I thought about is to define a special annotation like @FeatureXRestController
and make it @RestController
and put @ConditionalOnProperty
there but its still two places and its the best solution I could come up with:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@RestController
@ConditionalOnProperty(name = "myapp.featureX.enabled", havingValue = "true", matchIfMissing=true)
public @interface FeatureXRestController {
}
...
@FeatureXRestController
public class FeatureXRestController1 {...}
@FeatureXRestController
public class FeatureXRestController2 {...}
Solution
I've Found a relatively elegant workaround that might be helpful for the community:
I don't use a specialized meta annotation like I suggested in the question and annotate the controller of Feature X with the regular @RestController
annotation:
@RestController
public class FeatureXController {
...
}
The Spring boot application class can be "instructed" to not load RestControllers during the component scanning exclusion filter. For the sake of example in the answer I'll use the built-in annotation filter, but in general custom filters can be created for more sophisticated (real) cases:
// Note the annotation - component scanning process won't recognize classes annotated with RestController, so from now on all the rest controllers in the application must be defined in `@Configuration` classes.
@ComponentScan(excludeFilters = @Filter(RestController.class))
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
Now, since I want the rest controller to be loaded only if Feature X is enabled, I create the corresponding method in the FeatureXConfiguration:
@Configuration
@ConditionalOnProperty(value = "mayapp.featureX.enabled", havingValue = "true", matchIfMissing = false)
public class FeatureXConfiguration {
@Bean
public FeatureXService featureXService () {
return new FeatureXService();
}
@Bean
public FeatureXRestController featureXRestController () {
return new FeatureXRestController(featureXService());
}
}
Although component scanning process doesn't load the rest controllers, the explicit bean definition "overrides" this behavior and the rest controller's bean definition is created during the startup. Then Spring MVC engine analyzes it and due to the presence of the @RestController
annotation it exposes the corresponding end-point as it usually does.
Answered By - Mark Bramnik
Answer Checked By - Cary Denson (JavaFixing Admin)