Spring async doesn't work when implements AsyncConfigurer

17.1k views Asked by At

Having a Spring configuration class for async methods as:

@Configuration
@EnableAsync(proxyTargetClass = true)
@EnableScheduling
public class AsyncConfiguration {

@Autowired
private ApplicationContext applicationContext;

@Bean
public ActivityMessageListener activityMessageListener() {
    return new ActivityMessageListener();
}
@Bean
public TaskExecutor defaultExecutor()
{
    ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
    threadPoolTaskExecutor.setCorePoolSize(10);
    threadPoolTaskExecutor.setMaxPoolSize(10);
    threadPoolTaskExecutor.setQueueCapacity(Integer.MAX_VALUE);

    return threadPoolTaskExecutor;
}

All my @Asyncmethods works as expected but if I implement AsyncConfigurerinto AsyncConfiguration in order to catch exceptions implementing getAsyncUncaughtExceptionHandler() method, my beans are not being proxied so methods @Asyncdoesn't run in a pool executor.

This is the non-working configuration:

@Configuration
@EnableAsync(proxyTargetClass = true)
@EnableScheduling
public class AsyncConfiguration implements AsyncConfigurer {

@Autowired
private ApplicationContext applicationContext;

@Bean
public ActivityMessageListener activityMessageListener() {
    return new ActivityMessageListener();
}

@Override
public Executor getAsyncExecutor() {
    ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
    threadPoolTaskExecutor.setCorePoolSize(10);
    threadPoolTaskExecutor.setMaxPoolSize(10);
    threadPoolTaskExecutor.setQueueCapacity(Integer.MAX_VALUE);

    return threadPoolTaskExecutor;
}

@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler()        {
    return new SimpleAsyncUncaughtExceptionHandler();
}

What could be happening?

We are using @Async like this:

public class ActivityMessageListener extends BaseMessageListener {

public static final String PARAM_USER_ID                = "userId";
public static final String PARAM_COMPANY_ID             = "companyId";
public static final String PARAM_CREATE_DATE            = "createDate";
public static final String PARAM_CLASS_NAME             = "className";
public static final String PARAM_CLASS_PK               = "classPK";
public static final String PARAM_TYPE                   = "type";
public static final String PARAM_EXTRA_DATA             = "extraData";
public static final String PARAM_RECEIVED_USER_ID       = "receiverUserId";

@Override @Async(value = "defaultExecutor")
public Future<String> doReceive(Message message) throws Exception {

    String name = Thread.currentThread().getName();
    Map<String, Object> parameters  = message.getValues();
    Long userId                     = (Long)parameters.get(ActivityMessageListener.PARAM_USER_ID);
    Long companyId                  = (Long)parameters.get(ActivityMessageListener.PARAM_COMPANY_ID);
    Date createDate                 = (Date)parameters.get(ActivityMessageListener.PARAM_CREATE_DATE);
    String className                = (String)parameters.get(ActivityMessageListener.PARAM_CLASS_NAME);
    Long classPK                    = (Long)parameters.get(ActivityMessageListener.PARAM_CLASS_PK);
    Integer type                    = (Integer)parameters.get(ActivityMessageListener.PARAM_TYPE);
    String extraData                = (String)parameters.get(ActivityMessageListener.PARAM_EXTRA_DATA);
    Long receiverUserId             = (Long)parameters.get(ActivityMessageListener.PARAM_RECEIVED_USER_ID);
    ActivityLocalServiceUtil.addActivity(userId, companyId, createDate, className, classPK, type, extraData, receiverUserId);

    return new AsyncResult<String>(name);
}
}
1

There are 1 answers

1
Behrang Saeedzadeh On BEST ANSWER

EDIT: I have filed a bug report (SPR-14630).


I was on the verge of submitting a bug report to Spring's issue tracker, however when I was preparing a small app for reproducing the bug, I found and fixed the problem.

First of all, when using ThreadPoolTaskExecutor, you should call its initialize() method before returning it:

@Override
public Executor getAsyncExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setMaxPoolSize(1);
    executor.setCorePoolSize(1);
    executor.setThreadNamePrefix("CUSTOM-");

    // Initialize the executor
    executor.initialize();

    return executor;
}

Also for some reason, if I use a bean in a @PostConstruct method defined in the same configuration class, it won't run asynchronously. The reason is that the @PostConstruct method is executed before getAsyncExecutor() and getAsyncUncaughtExceptionHandler() are executed:

AsyncBean.java:

@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 %s", getClass().getSimpleName(), Thread.currentThread());

        System.out.println(message);
    }
}

AsyncDemoApp.java:

@SpringBootApplication
@EnableAsync
public class AsyncDemoApp implements AsyncConfigurer {

    @Autowired
    private IAsyncBean asyncBean;

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

    @Override
    public Executor getAsyncExecutor() {
        System.out.println("AsyncDemoApp.getAsyncExecutor");

        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setThreadNamePrefix("CUSTOM-");
        executor.initialize();

        return executor;
    }

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        System.out.println("AsyncDemoApp.getAsyncUncaughtExceptionHandler");
        return (throwable, method, objects)
                -> throwable.printStackTrace();
    }

    @PostConstruct
    public void start() {
        System.out.println("AsyncDemoApp.start");
        asyncBean.whoAmI();
    }
}

Output:

AsyncDemoApp.start
My name is AsyncBean and I am running in Thread[main,5,main]
AsyncDemoApp.getAsyncExecutor
AsyncDemoApp.getAsyncUncaughtExceptionHandler

However, if you use your bean after the application context is ready to use, it should all work as expected:

@SpringBootApplication
@EnableAsync
public class AsyncDemoApp implements AsyncConfigurer {

    public static void main(String[] args) {
        final ConfigurableApplicationContext context = SpringApplication.run(AsyncDemoApp.class, args);

        final IAsyncBean asyncBean = context.getBean(IAsyncBean.class);

        asyncBean.whoAmI();
    }

    @Override
    public Executor getAsyncExecutor() {
        System.out.println("AsyncDemoApp.getAsyncExecutor");

        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setThreadNamePrefix("CUSTOM-");
        executor.initialize();

        return executor;
    }

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        System.out.println("AsyncDemoApp.getAsyncUncaughtExceptionHandler");
        return (throwable, method, objects)
                -> throwable.printStackTrace();
    }
}

Another weird behaviour is that if you autowire the async bean in the same configuration class, the auto wiring is happening before the custom async executor is configured so the bean does not run asynchronously and it runs in the main thread. This can be verified by adding a @PostConstruct to AsyncBean and using a CommandLineRunner to run the app (personally I think this is a bug. The behaviour is very surprising to say the least):

AsyncBean with @PostConstruct:

@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 %s", getClass().getSimpleName(), Thread.currentThread());

        System.out.println(message);
    }

    @PostConstruct
    public void postConstruct() {
        System.out.println("AsyncBean is constructed");
    }
}

AsyncDemoApp implementing CommandLineRunner:

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

    @Autowired
    private IAsyncBean asyncBean;

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

    @Override
    public Executor getAsyncExecutor() {
        System.out.println("AsyncDemoApp.getAsyncExecutor");

        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setThreadNamePrefix("CUSTOM-");
        executor.initialize();

        return executor;
    }

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        System.out.println("AsyncDemoApp.getAsyncUncaughtExceptionHandler");
        return (throwable, method, objects)
                -> throwable.printStackTrace();
    }

    @Override
    public void run(String... args) throws Exception {
        System.out.println("AsyncDemoApp.run");
        asyncBean.whoAmI();
    }
}

Output:

AsyncBean is constructed
AsyncDemoApp.getAsyncExecutor
AsyncDemoApp.getAsyncUncaughtExceptionHandler
AsyncDemoApp.run
My name is AsyncBean and I am running in Thread[main,5,main]

One more thing! :) If you use ThreadPoolTaskExecutor, depending on your requirements, you might want to set its daemon property to true, otherwise your app will keep on running forever (this is not a big problem for Web/Worker apps). Here's what the JavaDoc of setDaemon(boolean) says:

Set whether this factory is supposed to create daemon threads, just executing as long as the application itself is running. Default is "false": Concrete factories usually support explicit cancelling. Hence, if the application shuts down, Runnables will by default finish their execution. Specify "true" for eager shutdown of threads which still actively execute a Runnable at the time that the application itself shuts down.