17

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 {...}
8
  • 1
    So you just want to start parts of your Application? Did you read about @Profile? This way you can differ which parts of your app run by a property which you can set as env var Commented Jun 13, 2020 at 5:44
  • Yes, exactly - so that the part will include rest controllers as well as other beans Commented Jun 13, 2020 at 5:46
  • With @Profile you can also create Application.properties per configuration if you Name it application-yourProfile.yml Commented Jun 13, 2020 at 5:50
  • 1
    Well, i use conditional which is a generalization of profile (since spring 4 @profile is implemented as a conditional) so the solution I've offered in the end of the question indeed uses this technique. I'm asking whether there is a better solution that I could come up with. Commented Jun 13, 2020 at 6:24
  • 1
    @Alexander.Furer Yeah, I believe you mean the solution I’ve described at the end of the question with @FeatureXRestController... The best solution I could come up with :) Thanks for your opinion. Probably if @RestController wasn’t defined as a @Component in spring it was better in this case... Commented Jun 13, 2020 at 21:05

5 Answers 5

10

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

  1. Create a simple Spring Web project using spring initializr

  2. Add a simple controller as a pojo

     public class MyController {
         public String sayHello(String who) {
             return "Hello " + who;
         }
     }
    
  3. 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();
     }
    
  4. Build and Run the app

     ./mvnw spring-boot:run
    

    or if you created a gradle project

     ./gradlew bootRun
    
  5. 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.

Sign up to request clarification or add additional context in comments.

2 Comments

I have used this solution and worked fine. Unfortunately it stopped working since SpringBoot 3.2.1, the bean is in the context, it has the @RestController annotation but I think somehow it ignores the rest annotations and does not expose any paths anymore. I opened an issue regarding this.
@PopescuM can you post a link to the issue you opened?
9

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.

4 Comments

I'll accept my own answer (yeah, it sounds weird, I know :) ) in 23 hours if no-one posts better solution
have a look at this on the extreme bottom of page docs.spring.io/spring-framework/docs/current/javadoc-api/… ie @bean annotation is going to be deprecated, So this solution will fail
@TanmayNaik Just the field autowire within the @Bean annotation is deprecated not the entire annotation.
You might also take a look at stackoverflow.com/a/65033714/974186
5

Another quick and hacky solution:

@RestController
interface IController {}

class MyController implements IController {...}

@Configuration
class MyConfiguration {
    @Bean
    public MyController myBeanClass() {
        return new MyController(...);
    }
}

Spring won't create bean automatically because "@Component" is on interface, but all mappings of controller will work.

1 Comment

This is the best hack. Not only does it actually work but you can keep your path mapping annotations on the controller implementation.
0

I like both the solutions presented above. However, I came up with another one which worked for me and is pretty clean.

So, I decided to create my beans only using @Configuration classes, and I gave up the @RestController annotations entirely. I put the @Configuration class for web controllers in a separate package, so that I can pass the class descriptor to the @ComponentScan or @ContextConfiguration annotations whenever I want to enable the creation of controller beans. The most important part is to add the @ResponseBody annotation to all controller classes, above the class name, to preserve the REST controller properties. Very clean.

The drawback is that the controller classes are not recognized by the @WebMvcTest annotation, and I need to create the beans for all my controllers every time I do a MockMvc test for one controller. As I have just four controllers though, I can live with it for now.

Comments

0

Isn't it a better idea to use the basePackages property of @ComponentScan to restrict what classes SB will scan? e.g. :-

@ComponentScan(basePackages = "org.myapp.config")

so all your configuration classes are in the org.myapp.config package.

Seems pretty neat to me?

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.