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 ComponentScan
s, 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@Import
s and @ComponentScan
s 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
fromDemoFeatureTestApp
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 @Import
ing 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 {
}