Skip to content
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

Potential AsyncConfigurer bug or mis-interpreted/missing documentation [SPR-14630] #19197

Closed
spring-issuemaster opened this issue Aug 26, 2016 · 6 comments
Assignees

Comments

@spring-issuemaster
Copy link
Collaborator

@spring-issuemaster spring-issuemaster commented Aug 26, 2016

Behrang Saeedzadeh opened SPR-14630 and commented

In the following small program, the AsyncConfigurer methods are invoked after asyncBean is injected and not only asyncBean doesn't use the custom executor, but it doesn't even use the default out of the box executor (i.e. SimpleAsyncTaskExecutor), hence its async method runs synchronously on the main thread:

@SpringBootApplication
@EnableAsync
public class AsyncConfigurerDemo implements AsyncConfigurer, CommandLineRunner {

	@Autowired
	private IAsyncBean asyncBean;

	public static void main(String[] args) {
		SpringApplication.run(AsyncConfigurerDemo.class, args);
	}

	@Override
    public Executor getAsyncExecutor() {
		System.out.printf("%d - AsyncConfigurerDemo.getAsyncExecutor\n", Counter.getAndIncrement());
		return new SimpleAsyncTaskExecutor("AsyncConfigurerDemo-");
	}

	@Override
	public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
		System.out.printf("%d - AsyncConfigurerDemo.getAsyncUncaughtExceptionHandler\n", Counter.getAndIncrement());
		return (ex, method, params) -> ex.printStackTrace();
	}


	@Override
	public void run(String... args) throws Exception {
		System.out.printf("%d - AsyncConfigurerDemo.run\n", Counter.getAndIncrement());
		asyncBean.whoAmI();
	}
}

@Component
public class AsyncBean implements IAsyncBean {

    @Override
    @Async
    public void whoAmI() {
        final String message =
                String.format("My name is %s and I am running in thread [%s]",
                        getClass().getSimpleName(),
                        Thread.currentThread().getName()
                );

        System.out.println(message);
    }

    @PostConstruct
    public void setup() {
        System.out.printf("%d - AsyncBean.setup\n", Counter.getAndIncrement());
    }
}

When running this program, it outputs:

0 - AsyncBean.setup
1 - AsyncConfigurerDemo.getAsyncExecutor
2 - AsyncConfigurerDemo.getAsyncUncaughtExceptionHandler
3 - AsyncConfigurerDemo.run
My name is AsyncBean and I am running in thread [main]

As it can be seen, asyncBean's whoAmI() method is running in the main thread.

If I change the AsyncConfigurerDemo and make it not implement AsyncConfigurer, the whoAmI() method runs asynchronously:

@SpringBootApplication
@EnableAsync
public class AsyncConfigurerDemo implements CommandLineRunner {

	@Autowired
	private IAsyncBean asyncBean;

	public static void main(String[] args) {
		SpringApplication.run(AsyncConfigurerDemo.class, args);
	}

	@Override
	public void run(String... args) throws Exception {
		System.out.printf("%d - AsyncConfigurerDemo.run\n", Counter.getAndIncrement());
		asyncBean.whoAmI();
	}
}

Which outputs:

0 - AsyncBean.setup
1 - AsyncConfigurerDemo.run
My name is AsyncBean and I am running in thread [SimpleAsyncTaskExecutor-1]

This looks like a bug to me. Especially, given Spring's bean lifecycle:

  1. Loads bean definitions (bean instances not created yet)
  2. Post processes bean definitions (bean instances not created yet)
  3. Instantiates beans ()
  4. Calls setter methods
  5. Invokes beans post processors
  6. Makes beans ready for use

I was expecting the configurer methods to run before the 3rd phase. However, it looks like the bean instances are created and their @PostConstruct methods executed before the AsyncConfigurer extension points are executed.

Could you please confirm if this is a bug or if this behaviour is as it should be? And if the behaviour is correct, is it documented somewhere?

I have attached the source code to this issue. There are two main classes: org.behrang.bugreports.scenario1.AsyncConfigurerDemo and org.behrang.bugreports.scenario2.AsyncConfigurerDemo.


Affects: 4.3.2

Reference URL: http://stackoverflow.com/questions/30730185/spring-async-doesnt-work-when-implements-asyncconfigurer/39158776

Attachments:

@spring-issuemaster

This comment has been minimized.

Copy link
Collaborator Author

@spring-issuemaster spring-issuemaster commented Aug 26, 2016

Behrang Saeedzadeh commented

I forgot to mention that if I change the code to get the bean from the application context, it works fine:

@SpringBootApplication
@EnableAsync
public class AsyncConfigurerDemo implements AsyncConfigurer {

    public static void main(String[] args) {
        final ConfigurableApplicationContext context = SpringApplication.run(AsyncConfigurerDemo.class, args);
        final IAsyncBean asyncBean = context.getBean(IAsyncBean.class);
        asyncBean.whoAmI();
    }

    @Override
    public Executor getAsyncExecutor() {
        System.out.printf("%d - AsyncConfigurerDemo.getAsyncExecutor\n", Counter.getAndIncrement());
        return new SimpleAsyncTaskExecutor("AsyncConfigurerDemo-");
    }

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        System.out.printf("%d - AsyncConfigurerDemo.getAsyncUncaughtExceptionHandler\n", Counter.getAndIncrement());
        return (ex, method, params) -> ex.printStackTrace();
    }
}

Running this program prints:

0 - AsyncConfigurerDemo.getAsyncExecutor
1 - AsyncConfigurerDemo.getAsyncUncaughtExceptionHandler
2 - AsyncBean.setup
My name is AsyncBean and I am running in thread [AsyncConfigurerDemo-1]

The following variant also works as expected:

@SpringBootApplication
@EnableAsync
public class AsyncConfigurerDemo implements AsyncConfigurer, CommandLineRunner {

    @Autowired
    private ApplicationContext context;

    public static void main(String[] args) {
        SpringApplication.run(AsyncConfigurerDemo.class, args);
    }

    @Override
    public Executor getAsyncExecutor() {
        System.out.printf("%d - AsyncConfigurerDemo.getAsyncExecutor\n", Counter.getAndIncrement());
        return new SimpleAsyncTaskExecutor("AsyncConfigurerDemo-");
    }

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        System.out.printf("%d - AsyncConfigurerDemo.getAsyncUncaughtExceptionHandler\n", Counter.getAndIncrement());
        return (ex, method, params) -> ex.printStackTrace();
    }

    @Override
    public void run(String... args) throws Exception {
        final IAsyncBean asyncBean = context.getBean(IAsyncBean.class);
        asyncBean.whoAmI();
    }
}

Which, again, prints:

0 - AsyncConfigurerDemo.getAsyncExecutor
1 - AsyncConfigurerDemo.getAsyncUncaughtExceptionHandler
2 - AsyncBean.setup
My name is AsyncBean and I am running in thread [AsyncConfigurerDemo-1]
@spring-issuemaster

This comment has been minimized.

Copy link
Collaborator Author

@spring-issuemaster spring-issuemaster commented Aug 26, 2016

Stéphane Nicoll commented

Thanks. That little sample is a bit weird. You're injecting a bean that should benefit from a feature in the configuration class that actually defines said feature. Field injection in a configuration class happens very early (and even more early if it is your main spring boot class). I agree this is confusing and maybe there's a way to fix that or to warn the user about it...

Is there a reason to actually use that setup for your project?

@spring-issuemaster

This comment has been minimized.

Copy link
Collaborator Author

@spring-issuemaster spring-issuemaster commented Aug 26, 2016

Behrang Saeedzadeh commented

Hi Stephane,

Thanks for the quick reply!

The app is fairly new (1 year old), but it was written by developers new to Spring so almost all the beans are injected to the app's @Configuration class and rather than using CommandLineRunner, it starts the heavy lifting from that configuration class's @PostConstruct method (it is a worker style app).

I am intending to refactor it, but I spent a day trying to enable async support for the app and I was scratching my head thinking why the async methods are not running asynchronously.

Now that I have found the culprit I can use a setup that works.

Having said that, it is still a bit weird that when the configuration class implements AsyncConfigurer the async methods don't even use the default async infrastructure and become synchronous.

Regards,
Behrang

@spring-issuemaster

This comment has been minimized.

Copy link
Collaborator Author

@spring-issuemaster spring-issuemaster commented Aug 26, 2016

Stéphane Nicoll commented

Yeah that part is weird, I'll have a look to it. Thanks for the report.

@spring-issuemaster

This comment has been minimized.

Copy link
Collaborator Author

@spring-issuemaster spring-issuemaster commented Aug 27, 2016

Behrang Saeedzadeh commented

Hi Stephane,

A couple of questions:

  • Do @Configuration classes have a different life cycle, order of dependency injection, etc. compared to other beans? If so, is this documented in the reference docs? I couldn't find this in the reference docs.
  • Also I have not been able to find information in the docs related to when methods defined by ...Configurer classes are invoked in the Spring lifecycle. It would be nice if this was explained in the reference docs.

Cheers,
Behrang

@spring-issuemaster

This comment has been minimized.

Copy link
Collaborator Author

@spring-issuemaster spring-issuemaster commented Sep 19, 2016

Stéphane Nicoll commented

Sorry for the late reply.

I had a deeper look at the code and I don't think there's much we can do at this point. When I run your app, I get the following warning

2016-09-19 14:25:50.532  INFO 58776 --- [           main] trationDelegate$BeanPostProcessorChecker : Bean 'asyncBean' of type [class com.example.AsyncBean] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)

That tells you that because you asked for this bean to be injected very early (in the spring boot application itself!) then no post processor could be applied on it. That's why it is synchronous: the async aspect doesn't get a chance to be applied and the raw bean is injected instead.

As a general rule, please don't inject a bean that should have behaviour X in the very same class that configures behaviour X. If you look at that class, you're asking to get "asyncBean" injected before any of the usual callback happens. But that class is customizing how async processing works. And you want async behaviour on "asyncBean".

Just move the async configuration to a separate class and you'll be fine. As a general rule, I wouldn't use field injection in the class flagged with @SpringBootApplication.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.

None yet
2 participants
You can’t perform that action at this time.