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 {...}
You can use local classes in the java config. E.e.
@Configuration
public class MyAppFeatureXConfiguration {
@Bean
public FeatureXRestController1 featureXRestController1(AutowireCapableBeanFactory beanFactory) {
@RestController
class FeatureXRestController1Bean extends FeatureRestController1 {
}
FeatureXRestController1Bean featureBean = new FeatureXRestController1Bean();
// You don't need this line if you use constructor injection
autowireCapableBeanFactory.autowireBean(featureBean);
return featureBean;
}
}
Then you can omit the @RestController annotation on the "real" implementation, but use the other annotations like @RequestMapping as usual.
@RequestMapping(...)
public class FeatureXRestController1 {
@RequestMapping(value="/somePath/{someId}", method=RequestMethod.GET)
public String findSomething(@PathVariable String someId) {
...
}
}
Sine the FeatureXRestController1 doesn't have a @RestController annotation, it is not a @Component anymore and thus will not be picked up through component scan.
The MyAppFeatureXConfiguration returns a bean that is a @RestController. This FeatureXRestController1Bean extends the FeatureXRestController1 and thus has all the methods and request mappings of the superclass.
Since the FeatureXRestController1Bean is a local class it is not included in a component scan. This does the trick for me ;)
EDIT
I have used this solution and worked fine. Unfortunately it stopped working since SpringBoot 3.2.1
I tested it with SpringBoot 3.2.3 and it worked.
Steps to reproduce
Create a simple Spring Web project using spring initializr
Add a simple controller as a pojo
public class MyController {
public String sayHello(String who) {
return "Hello " + who;
}
}
Create an ApplicationConfig
@Bean
public MyController myRestController(){
@RestController
class SpringRestController extends MyController {
@GetMapping(path = "sayHello")
@Override
public String sayHello(@RequestParam(name="who") String who) {
return super.sayHello(who);
}
}
return new SpringRestController();
}
Build and Run the app
./mvnw spring-boot:run
or if you created a gradle project
./gradlew bootRun
Access the endpoint http://localhost:8080/sayHello?who=Ren%C3%A9
PS: I even tried the autowiring I in my original answer. It worked too.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With