Skip to content

Ability to mark @Component to be ignored by component scan #28978

@xak2000

Description

@xak2000

Motivation:

There are several standard framework annotations already marked with @Component annotation.

E.g. @Controller, @ControllerAdvice etc.

These annotations have more than one responsibility:

  1. 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 in DispatcherServlet; @ExceptionHandler methods from @ControllerAdvice will be registered in ExceptionHandlerExceptionResolver etc.
  2. 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 @Controllers or @ControllerAdvices 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:

  1. 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 @ControllerAdvices will accidentally be package-scanned. This will make spring-boot-starter's auto-configurations work unexpectedly (usually creating very subtle bugs).
  2. 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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    in: coreIssues in core modules (aop, beans, core, context, expression)status: declinedA suggestion or change that we don't feel we should currently apply

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions