Hi,

when renovatebot tried to bump our spring-boot dependency from 3.2.0 to 3.2.1 our tests started to fail. It looks like its an issue in how we are running tests in our shared maven modules, which are not coming with their own SpringBootApplication - for example our security module, which provides Spring Components to the micro services.

When we want to test those shared modules (w/o any SpringApplication in that module), we created a dedicated SpringApplication in the test folder (also adding e.g. some test Controllers to check whether the security stuff is being handled correctly).

Our shared modules are not part of the regular ComponentScans, because they reside in their own packages. In order to "activate" (and add the components to the ApplicationContext) of the microservices, we are usually using a custom annotation like EnableFeatureXyz. This custom annotation defines@Imports and @ComponentScans directives, so the necessary functionality is loaded.

This approach worked fine pre 3.2.1 for our test setup.

Reproduce Repo: https://github.com/christian-kirschnick/sb-comp-scan-321-reg Comes with 2 modules: 1. demoapp, containing our microservice, which is practically doing nothing here. 2. featuremodule, containing the custom annotation EnableFeature, which is using ComponentScan to scan everything within that module/package. The module only defines one Component, which should be added to the AppContext. In order to test the features of featuremodule, there is a dedicated DemoFeatureTestApp in the test folder - which is making use of the EnableFeature annotation. I also set ComponentScan on that test app to something different, so that our component is not loaded by this test app, but rather by the enable annotation.

In the main pom, change the spring-boot-starter-parent version: -> Use 3.2.0 and run tests, all green -> Use 3.2.1 and run tests, broken

This problem only occurs in our feature module with this dedicated test app. The micro services are fine (both runtime & tests).

cheers, Christian

Comment From: wilkinsona

Thanks for the sample. The problem does not occur when Spring Framework is downgraded to 6.1.1 (add <spring-framework.version>6.1.1</spring-framework.version> to your pom's <properties>). I wonder if it's related to the changes made for https://github.com/spring-projects/spring-framework/issues/31704. We'll transfer this to the Framework team so that they can investigate.

Comment From: snicoll

Thanks for the sample. It only works with 3.2.0 though. If you run it with 3.1.7 it fails the same was as it fails in 3.2.1. The only window in which this works was because of a regression in 6.1.x that got fixed by #31704.

From that perspective, this does not look like a bug to me but something that never really worked. Back to what you're trying to do, I don't know if there is a way to support that while maintaining consistency with the use cases that we're already supporting. That @ComponentScan("disabled") in your example really means that you want to make sure this is what the application uses.

Comment From: sbrannen

The annotations on DemoApplication look correct to me.

But DemoFeatureTestApp looks like it only needs the @SpringBootApplication annotation.

What happens if you remove @ComponentScan("disabled") and @EnableDemoFeature from DemoFeatureTestApp on Spring Boot 3.2.1 or later?

Comment From: christian-kirschnick

The annotations on DemoApplication look correct to me.

But DemoFeatureTestApp looks like it only needs the @SpringBootApplication annotation.

What happens if you remove @ComponentScan("disabled") and @EnableDemoFeature from DemoFeatureTestApp on Spring Boot 3.2.1 or later?

Then it will work. But that's not the point of the test. This test module is basically doing two things, and I want to test both. First it exposes the feature enable annotation, and if it is being used in any Spring application, it should load its components into the ApplicationContext. Second it's providing the Feature. If I'm doing what you are suggesting, I'm only able to test whether the Feature is working as expecting, but I'm not able to test whether the annotations are properly running...

Comment From: sbrannen

I didn't quite follow what "doesn't work".

If I'm doing what you are suggesting, I'm only able to test whether the Feature is working as expecting, but I'm not able to test whether the annotations are properly running...

In any case, I see that you've updated your example, and it appears to me that you are now able to test what you want.

Is that correct?

Comment From: christian-kirschnick

@sbrannen The point is: If I remove the @ComponentScan("disabled") directive, all the features will be loaded "as collateral". However, loading should only happen explicitly via the annotation. Because I want to make my module available to others as well, and they will make use of those annotations - and I have to make sure that I properly tested those enabler annotations as well.

From that perspective, this does not look like a bug to me but something that never really worked.

I get what you are saying, but not sure whether I fully agree to this statement though.

I have enhanced my reproduce repo via https://github.com/christian-kirschnick/sb-comp-scan-321-reg/commit/55555356a3f0d50a31dd8381b8fcc71ac4b132f6, so we also better understand the full picture.

I added the following: 1. Two features, called Feature1 and Feature2. Feature1 is loaded via EnableFeature1ViaComponentScan annotation, which is making use of @ComponentScan directly. Feature2 is loaded via EnableFeature2ViaImportConfigPlusComponentScan, which is making use of @Importing a Feature2Configuration, which in turn comes with @ComponentScan. 2. The microservice is now making use of both annotations 3. Added a test for the microservice, checking that both features are available 4. The testapp is now making use of both annotations 5. I added dedicated tests for each test

So the testapp and the microservice app are basically the same. Both are making use of the enable annotations, and do not trigger any scanning on the feature packages themselves - this is only performed by the annotations. One would assume that both will yield the same results.

With boot 3.2.0, the tests will: | Service | Test | Result | |---|---|---| | Demoapp | DemoApplicationTest (load both features) | :heavy_check_mark: |
| Testapp | LoadFeature1ViaComponentScanTest | :heavy_check_mark: |
| Testapp | LoadFeature2ViaConfigAndCompScanTest | :heavy_check_mark: |

With boot 3.2.1 (and as you pointed out, also any other versions) | Service | Test | Result | |---|---|---| | Demoapp | DemoApplicationTest (load both features) | :heavy_check_mark: |
| Testapp | LoadFeature1ViaComponentScanTest | :x: |
| Testapp | LoadFeature2ViaConfigAndCompScanTest | :heavy_check_mark: |

So the question is: Is the @ComponentScan on a custom custom only supported on the main code, but not on the test domain? If that's the case, this looks (to me) hardly like a design-choice, but a bug :) The 3.2.0 behavior looks more like how it's supposed to work.

If @EnableFeature1ViaComponentScan is basically a not-supported scenario (which I believe is the most straight forward way to achieve what we want), is the recommendation to move to Annotation -> Import Configuration -> ComponentScan instead?

Comment From: sbrannen

So the question is: Is the @ComponentScan on a custom custom only supported on the main code, but not on the test domain?

@ComponentScan has the same behavior in main and test code.

If @EnableFeature1ViaComponentScan is basically a not-supported scenario (which I believe is the most straight forward way to achieve what we want),

There is nothing wrong with your implementation of @EnableFeature1ViaComponentScan.

@ComponentScan is supported as a meta-annotation.

However, as @snicoll pointed out, your local, direct application of the @ComponentScan("disabled") annotation on DemoFeatureTestApp overrides (and effectively hides) any @ComponentScan annotations that are present as meta-annotations.

is the recommendation to move to Annotation -> Import Configuration -> ComponentScan instead?

No. The recommendation is that you either declare one or more local @ComponentScan annotations OR one or more @ComponentScan meta-annotations.

If you combine local and meta-annotations for @ComponentScan, only the local @ComponentScan annotations will be honored.

To fix your tests, use the following annotations on DemoFeatureTestApp.

@SpringBootConfiguration
@EnableAutoConfiguration
@EnableFeature1ViaComponentScan
@EnableFeature2ViaImportConfigPlusComponentScan
public class DemoFeatureTestApp {
  // ...

By removing @ComponentScan("disabled"), you are instructing Spring to look for @ComponentScan meta-annotations.

By using @SpringBootConfiguration and @EnableAutoConfiguration directly instead of @SpringBootApplication, you are removing the @ComponentScan meta-annotation on @SpringBootApplication from the picture.

And that leaves you with a single @ComponentScan annotation on @EnableFeature1ViaComponentScan.

If you want to rewrite @EnableFeature2ViaImportConfigPlusComponentScan so that it declares @ComponentScan("com.example.demo.feature2") instead of @Import(Feature2Configuration.class), that also works. But make sure you remove (or comment out) @ComponentScan("com.example.demo.feature2") on Feature2Configuration.

In light of that, I am closing this issue as "works as designed".

Comment From: sbrannen

As a side note, if you want to continue to use @SpringBootApplication directly, you can can modify DemoFeatureTestApp as follows.

// The following effectively disables component scanning from the current package
// but retains all other features of @SpringBootApplication.
@SpringBootApplication(scanBasePackages = "nonexistent_base_package")
@EnableFeature1ViaComponentScan
@EnableFeature2ViaImportConfigPlusComponentScan
public class DemoFeatureTestApp {
  // ...

I assume @SpringBootApplication(scanBasePackages = "nonexistent_base_package") is what you originally hoped to achieve with @ComponentScan("disabled").


p.s. If you want to retain the default component scanning functionality of Spring Boot, you'll likely want to mimic the configuration used on @SpringBootApplication (which also happens to be documented in the class-level Javadoc for org.springframework.boot.context.TypeExcludeFilter).

For example:

@ComponentScan(
    basePackages = "com.example.demo.feature1",
    excludeFilters = {
        @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
        @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class)
    }
)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface EnableFeature1ViaComponentScan {
}