Motivation:
There are several standard framework annotations already marked with @Component
annotation.
E.g. @Controller
, @ControllerAdvice
etc.
These annotations have more than one responsibility:
- Main responsibility: They make some other framework's part to understand that this class should be used for something (depending on the annotation). E.g.
@Controller
will be registered inDispatcherServlet
;@ExceptionHandler
methods from@ControllerAdvice
will be registered inExceptionHandlerExceptionResolver
etc. - Second responsibility: They make the class to be subject for component-scan (and be registered as a bean).
Usually this is not a problem. But there are cases when you need to create such components conditionally using @Bean
annotation. E.g. if you create spring-boot-starter
with auto-configurations. In this case you usually explicitly define a @Configuraton
with some @ConditionalOn*
annotations that creates such beans.
Usually this works fine as these @Controller
s or @ControllerAdvice
s are not in a package, that is a sub-package of @SpringBootApplication
's package of consumer app, that uses this spring-boot-starter
. So, they just don't fall under the package scan of the consumer app.
But:
- As a developer of some
spring-boot-starter
library, you couldn't guarantee what package will be used by a consumer app. So, even if it's unlikely, it's still possible that package of your@ControllerAdvice
s will accidentally be package-scanned. This will makespring-boot-starter
's auto-configurations work unexpectedly (usually creating very subtle bugs). - As a developer of some
spring-boot-starter
library, to prevent these components to be package-scanned in your integration tests, you should use a different package for auto-configuration tests (could be problematic as it hides non-public classes from tests) or exclude potentially dangerous classes explicitly from component-scanning in your tests. Not doing this leads to subtle bugs in tests (e.g. false-positive tests).
For @Controller
annotation there is a workaround. You could just not mark a class with @Controller
annotation and mark it with @RequestMapping
instead. This will make DispatcherServlet
to catch the bean as a controller (as it searches for both @Controller
and @RequestMapping
as a marker).
For @ControllerAdvice
I didn't find any workarounds. This is the only annotation that makes ExceptionHandlerExceptionResolver
to find that bean and use it for setup exception handling.
There is similar issue spring-projects/spring-boot#7168. But it's easier to check if @Configuration
is actually an auto-configuration.
I think it's impossible to automatically check if @Component
is actually not intended to be a component, but a bean that is conditionally created by some (auto-)configuration (and is marked with @Component
as an unwelcome side effect of other annotation).
Possible solutions
This is why I think about some dedicated annotation that will handle as an ignore-marker for component scan. As a developer of some spring-boot-starter
library, you will be able to mark any meta-annotated @Component
with this new annotation and be sure this class will never be picked up by component scan of consumer application or your integration tests.
Another solution for a concrete problem with @ControllerAdvice
is to add some other mechanism to register controller advice @Bean
without @ControllerAdvice
annotation. Maybe a marker interface or some new annotation (@ControllerAdviceNoComponent
? :-) ), that is not annotated with @Component
.
But this is obviously will solve only one exact case.
I want a good universal way to allow a library developer to make sure a class will never be catched up by a component scan of consuming app, like it's not annotated with @Component
at all. This will help not only with standard spring-framework annotations (like @ControllerAdvice
), but also with any 3rd party annotations, that also marked with @Component
for one reason or another.
Possible workaround
Add @Conditional
with always-false condition to a class that is annotated (directly or indirectly) with @Component
annotation.
E.g.:
@ConditionalOnExpression("false")
@ControllerAdvice
public class MyExceptionHandlerControllerAdvice {}
This makes @Component
to be ignored by component scan, but when you explicitly create @Bean
with this type, the @Conditional
on the type is ignored (only @Conditional
on bean-factory-method or parent @Configuration
is considered). So you are able to create a @Bean
, if needed.
But it also interferes with some other mechanisms, e.g. if you try to @Import
such component, it will not be imported (bean will not be created), while a simple class without any annotations will be imported. This makes a class that is annotated with both @Component
and @ConditionalOnExpression("false")
not exactly equal to a class that is not annotated at all. So, a dedicated annotation that only affects component scan and not interferes with @Import
or any other mechanisms will be preferred.
Context
I tried to fix this problem zalando/problem-spring-web#802 and found out that the problem was not noticed earlier because of false-positive tests that should be failed long time ago, but was successful because @ControllerAdvice
was registered in tests earlier than it is registered in a real application, that consumes problem-spring-web-starter. In a real application @ControllerAdvice
was registered by auto-configuration (that fires later), while in tests it was registered by component scan and this fact itself leads to ordering problem, that in the end leads to early ExceptionHandlerExceptionResolver
registration (when @ControllerAdvice
from auto-configuration is not loaded yet).
Comment From: bclozel
For
@Controller
annotation there is a workaround. You could just not mark a class with@Controller
annotation and mark it with@RequestMapping
instead. This will makeDispatcherServlet
to catch the bean as a controller (as it searches for both@Controller
and@RequestMapping
as a marker).
This behavior has been removed in #22154. The @Controller
annotation has always implied that the type should be considered as a scannable component. I understand the rationale behind this issue, but I don't think we should introduce another level of complexity here, whereas the root issue is really about structuring a Spring Boot starter and its test suite.
This is being called out explicitly in the Spring Boot reference documentation.
Auto-configurations must be loaded only by being named in the imports file. Make sure that they are defined in a specific package space and that they are never the target of component scanning. Furthermore, auto-configuration classes should not enable component scanning to find additional components. Specific @Import annotations should be used instead.
Large projects like Spring Boot and popular starters manage this pretty well. A starter could also consider defining its own flavor of sliced tests with an annotation filter to only consider the relevant beans.
I'm declining this enhancement request as a result, see also #29650.
Comment From: xak2000
@bclozel
Make sure that they are defined in a specific package space and that they are never the target of component scanning.
I'm not sure how a library's developer could reliably do this. No matter what package you will use for your library, it's always a possibility that an end-user of your library will use the same package (or it's parent) for their @SpringBootApplication
class, so packages of your library will be the target of component scanning and you can't influence it in any way. What am I missing?
I understand that it's only a probability, but when you develop a library, you want not probability, but confidence, right? That probability is much higher if starter/library is developed by the same company that has many independent teams that will use this starter then. In this case it's not hard to imagine that starter from com.mycompany.mylib.autoconfiguration
package will be used by some team, that uses com.mycompany
package for their main (annotated) class. It could be solved by administrative rules in this case, but it would be much better to have an ability to write bulletproof starters, that don't depend on package structure you have no control of.
Also I don't understand why do you think this will introduce complexity:
I don't think we should introduce another level of complexity here
In my opinion it will add flexibility while preserving the complexitiy on the same level, as nothing will change for generic scenarios.
As a heavy user of Spring Framework and Spring Boot (10+ years now) I spent too much time "fighting the framework" because it doesn't allow me to do things that are supposed to be done very simply. When you must spent several hours to understand how could you create MVC request handler, that will not be a target for component-scan - that is another level of complexity. :) And if I understand you correctly, now this is even impossible because there is no such annotation exist anymore (#22154).
We clearly have violation of single responsibility principle here. One annotation serves 2 purposes: be a bean and be a MVC request handler. Why @Controller
or @ControllerAdvice
or any other framework-defined building blocks are meta-annotated with @Component
(so forced to be a bean) in the first place? The only reason is to cover 95% of usage scenarios, i.e. to allow end-users to use the framework in a simple way.
I think we all agree that it would be very inconvenient and error-prone to force users to add both @Controller
and @Component
to their controllers. So, I fully understand and agree that 95% of usage scenarios should be done simple.
But what about other 5%? If the answer is "They should be allowed to do, but not so simple" then OK, but current answer is "They are impossible to do".
There is an easy and more flexible way to solve this violation of SRP: composition. We could have an annotation that is dedicated to be a marker for controller (e.g. @BaseController
) and only this annotation will be considered by MVC Framework for registration of a request handler. How a bean is created - it's not the business of MVC Framework. Then, we could also create a composite annotation @Controller
(already exists) that will be meta-annotated by both @Component
and @BaseController
. This way it will be win-win. 95% scenarios will still be covered without any changes, while flexibility will be preserved (or, rather, achieved).
The only downside I see is that 1 more annotation need to be created per Spring Framework concept. @Controller
, @RestController
, @ControllerAdvice
, @Repository
and any other annotations that are currently used for both auto-scanning and to perform concrete actions inside of framework (handle requests, translate exceptions etc) - all they need to be paired with some another new (separate) annotation (and naming is hard!). But this is small price for such great safety and simplification: you writing a library? Then just use "base" annotations and never use their bean-counterparts. With https://github.com/spring-projects/spring-boot/issues/7168 implemented there will be no way to accidentally mess up with tests or clash of packages if this simple rule is followed.
Comment From: reda-alaoui
In this case it's not hard to imagine that starter from com.mycompany.mylib.autoconfiguration package will be used by some team, that uses com.mycompany package for their main (annotated) class.
This is exactly the risk we are exposed to. It already happened in one of our team.
Comment From: bclozel
The only downside I see is that 1 more annotation need to be created per Spring Framework concept.
@Controller
,@RestController
,@ControllerAdvice
,@Repository
and any other annotations that are currently used for both auto-scanning and to perform concrete actions inside of framework (handle requests, translate exceptions etc) - all they need to be paired with some another new (separate) annotation (and naming is hard!).
I understand the problem here, but in my opinion introducing new @Base***
annotations for all existing features is likely to confuse our user base. Why add to the cognitive load of all users for a 5% use case? How can we prevent developers from using those annotations in a package that is should be scanned? By solving this problem for library developers, we're creating a much bigger one for application developers and adding new public, user-facing types.
With https://github.com/spring-projects/spring-boot/issues/7168 implemented there will be no way to accidentally mess up with tests or clash of packages if this simple rule is followed.
This approach is already implemented and my previous comment is relevant to that: creating a custom sliced test annotation/filter should take care of the testing part in the library itself.
As for developers scanning the library's package, I think that the way out here is to use the functional variants, or functional registrations. For example, Spring for GraphQL is contributing web endpoints without any @Controller
annotation.
Comment From: xak2000
creating a custom sliced test annotation/filter should take care of the testing part in the library itself.
This approach is very brittle. Especially for auto-configuration tests, where you need to make sure that there is no interference between auto-configurations, ordering, life-cycles etc. The problem with sliced tests is that they don't represent the environment as it would be in a real app, that will use that starter. It's because these tests are.. sliced. :)
I mentioned spring-projects/spring-boot#7168 because it tries to solve the same problem, but for auto-configurations (that are not intended to be component-scanned; the same way as @Controller
or @ControllerAdvice
are not intended to be component-scanned if they are created by these auto-configurations). It solves this problem using filtering, right. But it's possible to use filtering here only because it's possible to create appropriate conditions (all auto-configurations are always explicitly present in spring.factories
). If it would be possible to create a condition for "a bean, that is normally target for auto-scan, but it's already exposed by some auto-configuration", then it would be definitely useful filter that is worth to be included as part of @SpringBootApplication
annotation the same way as AutoConfigurationExcludeFilter
is included. But I don't see any possiblity to create such condition.
I disagree that library developers should suffer that much trying to do a very basic things (like to create @Bean
programmatically without fear that these beans also will be component-scanned in some cases, totally unintentionally), but I could live with this if there is at least some (maybe not that easy) way to do this. At this point I don't know any way. Probably it's possible to create a filter that will use a manually written condition, that includes all library's components (that are possible targets to component-scan). Or this filter could use some marker-annotation etc (also specific for a concrete library, that in my opinion not a very good thing, as this is very useful feature for any starter/library). But there is no way to force this filter to be used by the app, that uses this library, right? The only way to register this filter is to add it to @ComponentScan
annotation. Maybe I'm missing something and there is an other way (that could be used by library)? I honestly asking because I really hope I just missed some possibility, as it was long time ago when I re-read the whole docs last time, and Framework is constantly improving! (still not jumped to 6.x train)
As for developers scanning the library's package, I think that the way out here is to use the functional variants, or functional registrations.
It's not the same as registering a controller bean, right? For example I want to provide a default Controller in the starter, while allow user to define their own controller (deafult controller should back off in this case). This is done for BasicErrorController
in spring-boot, for example (see ErrorMvcAutoConfiguration
).
I understand the problem here, but in my opinion introducing new @Base*** annotations for all existing features is likely to confuse our user base.
Maybe you are right. It's too much changes at this Framework stage. It's pity I didn't came with this problem earlier at Spring 3.0 times, maybe even earlier. :)
That is why I thought about some more generic non-intrusive solution, like marker interface/annotation that will be handled by some out-of-the-box component-scanning filter that's always active. I don't see how this interface/annotation will harm or confuse anyone as there is basically no reason to use it except when you really need it. No one can "accidentally" add it to their components, that are intended to be target for component-scan. :)
To be honest, at this stage I tired to try to convince. :) It's very clear to me that this feature would be very useful as I suffered from it's absence too often. But maybe there are potential quirks in such changes that will affect some other parts of Framework unexpectedly (e.g. interaction with @Import
or anything else). It's the only thing I'm afraid in relation to such change: fear that it's probably not that backward-compatible and "safe" as it looks like. :)