Skip to content

Native image ignoring annotations when actual bean type is not exposed in the @Bean method #32527

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Schaka opened this issue Mar 25, 2024 · 5 comments
Labels
status: invalid An issue that we don't feel is valid

Comments

@Schaka
Copy link

Schaka commented Mar 25, 2024

I recently migrated my project to native images. As @ConditionalOnProperty doesn't work with native images, I manually created my beans in @Configuration classes dependent on properties.

The @Bean method returns an interface, like such

@Configuration(proxyBeanMethods = false)
class RadarrConfig(
    //val radarrRestService: RadarrRestService,
    val radarrNoOpService: RadarrNoOpService
) {

    @Bean
    @Radarr
    fun radarrService(
        radarrProperties: RadarrProperties,
        radarrClient: RadarrClient,
        applicationProperties: ApplicationProperties,
        fileSystemProperties: FileSystemProperties
    ): ServarrService {

        if (radarrProperties.enabled) {
            return RadarrRestService(radarrClient, applicationProperties, fileSystemProperties, radarrProperties)
            //return radarrRestService
        }

        return radarrNoOpService
    }
}

The RadarrRestService has a @Cacheable annotation on one method, @PostConstruct on another and @RegisterReflectionForBinding on the class itself.

None of the annotations are being processed by Spring.

Here are some observations as to what works:

  • moving the postConstruct method into ServarrService and annotating the interface with @PostConstruct makes it get called
  • annotating the RadarrRestService class with @Service results in 2 beans, but BOTH will have their @Cacheable processed, including the one created via @Bean
  • @RegisterReflectionForBinding gets processed only when RadarrRestService is annotated with @Service

Other attempts/observations:
I had tried letting Spring create my beans with @Service annotations and only letting my config class device which already injected bean gets returned by the @Bean method - like here.

Unfortunately, this would throw a startup exception inside the native image (but not local AOT application startup), because RadarrService already had a CGLib class (proxy?) surrounding it. I don't have the exact exception anymore, unfortunately.

From what I can tell, this isn't actually intended behavior. It almost feels like there's only a small switch that was forgotten so that would allow @Bean produced beans to have their annotations (on all classes and subclasses) processed correctly.

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Mar 25, 2024
@Schaka Schaka changed the title Native images ignoring annotations when beanis created via @Bean Native images ignoring annotations when bean is created via @Bean Mar 25, 2024
@bclozel bclozel transferred this issue from spring-projects/spring-boot Mar 25, 2024
@bclozel
Copy link
Member

bclozel commented Mar 25, 2024

I've had a look at your application and unfortunately it's too large for us to consider it in this issue.

I think the main misunderstanding here is that the @ConditionalOnProperty limitation is linked to the annotation and that there is another way to support this. This is unfortunately not true. The Spring Boot documentation explains this difference:

Code that cannot be reached when the native image is created will be removed and won’t be part of the executable.

This means that if a bean is not present when the application is built, its definition will not be present at runtime in native mode, even if th environment changes. Removing unused code paths is a key feature in GraalVM - if we were to somewhat work around that, this would mean that applications would get significantly larger as we would ship way more code than expected. This is being tracked in #21497 but we haven't made progress on this task as the existing tradeoffs work quite well from what we're seeing.

The other issues listed here might be linked to AOT best practices not being followed - if the behavior is too dynamic, the AOT engine cannot infer bean information and won't be able to have the behavior you're expecting.

If you'd like to continue here, let's consider issues one by one and please provide minimal samples for each. This makes the process a bit more cumbersome for you, but isolating problems is the best way to be on the same page. Thanks!

@bclozel bclozel added the status: waiting-for-feedback We need additional information before we can continue label Mar 25, 2024
@Schaka
Copy link
Author

Schaka commented Mar 25, 2024

Thank you for your quick response.

I don't mind creating a minimal reproducible project for you to illustrate the "bugs". But after reading through the AOT best practices docs you linked, I just don't think what I'm trying to achieve is possible.

If I cannot return an interface and expect the actual implementation to have its annotations processed, most of my points are moot. I'll have to create both beans at all times and then decide which one to use at runtime.

If you think it's worth having a sample project, I can set it up, but knowing what I know now, it would likely be a waste of both our time.

The only thing I found to be interesting is @Service essentially serving as a meta annotation and being 'taught' how to process the result of the @Bean method. It seems to be a side effect, but might be a good feature to consider.

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels Mar 25, 2024
@snicoll
Copy link
Member

snicoll commented Mar 25, 2024

If I cannot return an interface and expect the actual implementation to have its annotations processed, most of my points are moot. I'll have to create both beans at all times and then decide which one to use at runtime.

Things are processed at build-time so you can't hide the actual implementation. If you do, then usual containers callbacks won't be processed on your types. That said, there are several ways, depending on the actua callback to address this issue.

The only thing I found to be interesting is @service essentially serving as a meta annotation and being 'taught' how to process the result of the @bean method. It seems to be a side effect, but might be a good feature to consider.

I have no idea what that means. Perhaps there is still a misunderstanding and a sample would help still? If you don't intend to share it, please close the issue. Otherwhise, make it as focused as possible so that we know we're on the same page.

@snicoll snicoll added status: waiting-for-feedback We need additional information before we can continue and removed status: feedback-provided Feedback has been provided labels Mar 25, 2024
@Schaka
Copy link
Author

Schaka commented Mar 25, 2024

Thanks for working with me! I have managed to create a small project illustrating my use-case.
I'm now sure that there's an actual bug I found or at least inconsistent behavior between AOT and the compiled native image.

Given this project,@PostProcessing annotations for both @Bean created beans are ignored. Starting the application with -Dspring.aot.enabled=true will process the @Cachable annotation correctly. When compiled to a native image, this isn't the case.

Output from native image

2024-03-25T12:31:17.993Z  INFO 1 --- [   scheduling-1] c.g.s.n.radarr.RadarrRestService         : RadarrService called com.github.schaka.native_example.radarr.RadarrRestService@7e76e573
2024-03-25T12:31:17.994Z  INFO 1 --- [   scheduling-1] c.g.schaka.native_example.SomeService    : From com.github.schaka.native_example.radarr.RadarrRestService@7e76e573 Counter: [1]
2024-03-25T12:31:17.994Z  INFO 1 --- [   scheduling-1] c.g.schaka.native_example.SomeService    : From com.github.schaka.native_example.radarr.RadarrRestService@7e76e573 Counter: [1]
2024-03-25T12:31:17.995Z  INFO 1 --- [   scheduling-1] c.g.s.n.sonarr.SonarrRestService         : SonarrService called com.github.schaka.native_example.sonarr.SonarrRestService@3c1a8c89
2024-03-25T12:31:17.995Z  INFO 1 --- [   scheduling-1] c.g.schaka.native_example.SomeService    : From com.github.schaka.native_example.sonarr.SonarrRestService@3c1a8c89 Counter: [1]
2024-03-25T12:31:17.995Z  INFO 1 --- [   scheduling-1] c.g.s.n.sonarr.SonarrRestService         : SonarrService called com.github.schaka.native_example.sonarr.SonarrRestService@3c1a8c89
2024-03-25T12:31:17.995Z  INFO 1 --- [   scheduling-1] c.g.schaka.native_example.SomeService    : From com.github.schaka.native_example.sonarr.SonarrRestService@3c1a8c89 Counter: [2]
2024-03-25T12:31:27.995Z  INFO 1 --- [   scheduling-1] c.g.schaka.native_example.SomeService    : From com.github.schaka.native_example.radarr.RadarrRestService@7e76e573 Counter: [1]
2024-03-25T12:31:27.995Z  INFO 1 --- [   scheduling-1] c.g.schaka.native_example.SomeService    : From com.github.schaka.native_example.radarr.RadarrRestService@7e76e573 Counter: [1]
2024-03-25T12:31:27.995Z  INFO 1 --- [   scheduling-1] c.g.s.n.sonarr.SonarrRestService         : SonarrService called com.github.schaka.native_example.sonarr.SonarrRestService@3c1a8c89
2024-03-25T12:31:27.995Z  INFO 1 --- [   scheduling-1] c.g.schaka.native_example.SomeService    : From com.github.schaka.native_example.sonarr.SonarrRestService@3c1a8c89 Counter: [3]
2024-03-25T12:31:27.995Z  INFO 1 --- [   scheduling-1] c.g.s.n.sonarr.SonarrRestService         : SonarrService called com.github.schaka.native_example.sonarr.SonarrRestService@3c1a8c89
2024-03-25T12:31:27.995Z  INFO 1 --- [   scheduling-1] c.g.schaka.native_example.SomeService    : From com.github.schaka.native_example.sonarr.SonarrRestService@3c1a8c89 Counter: [4]

Output from AOT build:

2024-03-25T13:39:29.444+01:00  INFO 8953 --- [   scheduling-1] c.g.s.n.radarr.RadarrRestService         : RadarrService called com.github.schaka.native_example.radarr.RadarrRestService@6f1a16fe
2024-03-25T13:39:29.446+01:00  INFO 8953 --- [   scheduling-1] c.g.schaka.native_example.SomeService    : From com.github.schaka.native_example.radarr.RadarrRestService@6f1a16fe Counter: [1]
2024-03-25T13:39:29.450+01:00  INFO 8953 --- [   scheduling-1] c.g.schaka.native_example.SomeService    : From com.github.schaka.native_example.radarr.RadarrRestService@6f1a16fe Counter: [1]
2024-03-25T13:39:29.450+01:00  INFO 8953 --- [   scheduling-1] c.g.s.n.sonarr.SonarrRestService         : SonarrService called com.github.schaka.native_example.sonarr.SonarrRestService@75120e58
2024-03-25T13:39:29.450+01:00  INFO 8953 --- [   scheduling-1] c.g.schaka.native_example.SomeService    : From com.github.schaka.native_example.sonarr.SonarrRestService@75120e58 Counter: [1]
2024-03-25T13:39:29.450+01:00  INFO 8953 --- [   scheduling-1] c.g.schaka.native_example.SomeService    : From com.github.schaka.native_example.sonarr.SonarrRestService@75120e58 Counter: [1]
2024-03-25T13:39:39.454+01:00  INFO 8953 --- [   scheduling-1] c.g.schaka.native_example.SomeService    : From com.github.schaka.native_example.radarr.RadarrRestService@6f1a16fe Counter: [1]
2024-03-25T13:39:39.455+01:00  INFO 8953 --- [   scheduling-1] c.g.schaka.native_example.SomeService    : From com.github.schaka.native_example.radarr.RadarrRestService@6f1a16fe Counter: [1]
2024-03-25T13:39:39.455+01:00  INFO 8953 --- [   scheduling-1] c.g.schaka.native_example.SomeService    : From com.github.schaka.native_example.sonarr.SonarrRestService@75120e58 Counter: [1]
2024-03-25T13:39:39.455+01:00  INFO 8953 --- [   scheduling-1] c.g.schaka.native_example.SomeService    : From com.github.schaka.native_example.sonarr.SonarrRestService@75120e58 Counter: [1]

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels Mar 25, 2024
@snicoll
Copy link
Member

snicoll commented Mar 25, 2024

Alright. Yes, if you use annotations in an implementation you hide from us, there is no way to find out about the stereotypes you want us to process in a native image.

For this particular case:

  • @PostConstruct is detected at build-time and we make sure it's registered so that the code does not have to deal with this at runtime. You could easily avoid the problem by implementing InitializingBean and that will be invoked regardless
  • Your cache use will not create a JDK proxy as you seem to believe (given the hints you've added). It will create a CGLIB proxy and this has to be created at build-time. Also, we do need reflection hints for all @Cacheable use. You can see that this annotation is meta-annotated with org.springframework.aot.hint.annotation.Reflective to indicate reflection hints must be created by AOT. You can workaround that by registering the hints manually.

I've done both of that in and this outputs the following in a Native image for me:

2024-03-25T16:34:15.482+01:00  INFO 18953 --- [           main] c.g.s.n.radarr.RadarrConfig              : Manually created RadarrService com.github.schaka.native_example.radarr.RadarrRestService@570bfbee
2024-03-25T16:34:15.482+01:00  INFO 18953 --- [           main] c.g.s.n.radarr.RadarrRestService         : PostConstruct called from com.github.schaka.native_example.radarr.RadarrRestService@570bfbee
2024-03-25T16:34:15.483+01:00  INFO 18953 --- [           main] c.g.s.n.sonarr.SonarrConfig              : Manually created SonarrService com.github.schaka.native_example.sonarr.SonarrRestService@199a5f9
2024-03-25T16:34:15.483+01:00  INFO 18953 --- [           main] c.g.s.n.sonarr.SonarrRestService         : PostConstruct called from com.github.schaka.native_example.sonarr.SonarrRestService@199a5f9
2024-03-25T16:34:15.483+01:00  INFO 18953 --- [           main] c.g.s.n.radarr.RadarrRestService         : PostConstruct called from com.github.schaka.native_example.radarr.RadarrRestService@544c7210
2024-03-25T16:34:15.531+01:00  INFO 18953 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port 8080 (http) with context path ''
2024-03-25T16:34:15.531+01:00  INFO 18953 --- [           main] c.g.s.n.NativeExampleApplicationKt       : Started NativeExampleApplicationKt in 0.124 seconds (process running for 0.154)
2024-03-25T16:34:15.532+01:00  INFO 18953 --- [   scheduling-1] c.g.s.n.radarr.RadarrRestService         : RadarrService called com.github.schaka.native_example.radarr.RadarrRestService@570bfbee
2024-03-25T16:34:15.533+01:00  INFO 18953 --- [   scheduling-1] c.g.schaka.native_example.SomeService    : From com.github.schaka.native_example.radarr.RadarrRestService@570bfbee Counter: [1]
2024-03-25T16:34:15.533+01:00  INFO 18953 --- [   scheduling-1] c.g.schaka.native_example.SomeService    : From com.github.schaka.native_example.radarr.RadarrRestService@570bfbee Counter: [1]
2024-03-25T16:34:15.533+01:00  INFO 18953 --- [   scheduling-1] c.g.s.n.sonarr.SonarrRestService         : SonarrService called com.github.schaka.native_example.sonarr.SonarrRestService@199a5f9
2024-03-25T16:34:15.533+01:00  INFO 18953 --- [   scheduling-1] c.g.schaka.native_example.SomeService    : From com.github.schaka.native_example.sonarr.SonarrRestService@199a5f9 Counter: [1]
2024-03-25T16:34:15.533+01:00  INFO 18953 --- [   scheduling-1] c.g.schaka.native_example.SomeService    : From com.github.schaka.native_example.sonarr.SonarrRestService@199a5f9 Counter: [1]

I've pushed the code I've changed so that you can review it: https://github.com/Schaka/native-example/pull/1

With all that said, I wouldn't recommend that setup if you work with Native images as it requires too many manual steps and we're already document that's not going to change.

Hope that helps.

@snicoll snicoll closed this as not planned Won't fix, can't repro, duplicate, stale Mar 25, 2024
@snicoll snicoll added status: invalid An issue that we don't feel is valid and removed status: waiting-for-triage An issue we've not yet triaged or decided on status: feedback-provided Feedback has been provided labels Mar 25, 2024
@snicoll snicoll changed the title Native images ignoring annotations when bean is created via @Bean Native image ignoring annotations when actual bean type is not exposed in the @Bean method Mar 25, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
status: invalid An issue that we don't feel is valid
Projects
None yet
Development

No branches or pull requests

4 participants