The Thirty-seventh chapter of Spring’s Road to God: @EnableAsync & @Async implements method asynchronous invocation

1. Content of this article

Explain @EnableAsync & amp; @Async in detail, mainly in the following points.

  1. Action
  2. Usage
  3. Get asynchronous execution results
  4. Customize the thread pool for asynchronous execution
  5. Custom exception handling
  6. Thread isolation
  7. Source code & amp; principle

2. Function

The asynchronous invocation of the bean method is implemented in the spring container.

For example, if there is a bean of logService, there is a log method in logservice to record logs. When calling logService.log(msg), if you want to execute it asynchronously, you can pass @EnableAsync & amp ; @Async to achieve.

3. Usage

2 steps

  1. The methods that need to be executed asynchronously are marked with the @Async annotation. If all the methods in the bean need to be executed asynchronously, you can directly load the @Async on the class.
  2. Add @EnableAsync to the spring configuration class, then the @Async annotation will take effect.

Two common usages

  1. no return value
  2. can get the return value

4. No return value

Usage

The return value of the method is not of type Future. When it is executed, it will return immediately, and the return value of the method cannot be obtained, such as:

@Async
public void log(String msg) throws InterruptedException {
    System.out.println("Start logging," + System.currentTimeMillis());
    //Simulation takes 2 seconds
    TimeUnit. SECONDS. sleep(2);
    System.out.println("The logging is complete," + System.currentTimeMillis());
}

Case

Realize the function of log asynchronous recording.

The LogService.log method is used to record logs asynchronously and needs to be marked with @Async

package com.javacode2018.async.demo1;

import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

@Component
public class LogService {
    @Async
    public void log(String msg) throws InterruptedException {
        System.out.println(Thread.currentThread() + "Start logging," + System.currentTimeMillis());
        //Simulation takes 2 seconds
        TimeUnit. SECONDS. sleep(2);
        System.out.println(Thread.currentThread() + "logging is complete," + System.currentTimeMillis());
    }
}

Come to a spring configuration class, you need to add @EnableAsync to open the asynchronous call of the bean method.

package com.javacode2018.async.demo1;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.scheduling.annotation.EnableAsync;

@ComponentScan
@EnableAsync
public class MainConfig1 {
}

test code

package com.javacode2018.async;

import com.javacode2018.async.demo1.LogService;
import com.javacode2018.async.demo1.MainConfig1;
import org.junit.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import java.util.concurrent.TimeUnit;

public class AsyncTest {

    @Test
    public void test1() throws InterruptedException {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
        context.register(MainConfig1.class);
        context. refresh();
        LogService logService = context. getBean(LogService. class);
        System.out.println(Thread.currentThread() + "logService.log start," + System.currentTimeMillis());
        logService.log("Asynchronous execution method!");
        System.out.println(Thread.currentThread() + "logService.log end," + System.currentTimeMillis());

        //Sleep to prevent @Test from exiting
        TimeUnit. SECONDS. sleep(3);
    }

}

run output

Thread[main,5,main] logService.log start,1595223990417
Thread[main,5,main] logService.log end,1595223990432
Thread[SimpleAsyncTaskExecutor-1,5,main] starts logging, 1595223990443
Thread[SimpleAsyncTaskExecutor-1,5,main] logging completed, 1595223992443

From the output of the first 2 lines, it can be seen that logService.log returns immediately, and the next 2 lines come from the log method, with a difference of about 2 seconds.

The first 2 lines are executed in the main thread, and the latter 2 lines are executed in the asynchronous thread.

5. Get asynchronous return value

Usage

If you need to get the asynchronous execution result, the method return value must be of type Future, use the static method org.springframework.scheduling.annotation.AsyncResult#forValue provided by spring to create the return value, like:

public Future<String> getGoodsInfo(long goodsId) throws InterruptedException {
    return AsyncResult.forValue(String.format("basic information of product %s!", goodsId));
}

Case

Scenario: Product detail pages in e-commerce usually have a lot of information: basic product information, product description information, and product review information. There are three ways to get or these pieces of information.

These three methods are unrelated, so they can be obtained in parallel in an asynchronous manner to improve efficiency.

The following is a commodity service. All three internal methods need to be asynchronous, so they are directly marked with @Async on the class. Each method sleeps for 500 milliseconds internally to simulate time-consuming operations.

package com.javacode2018.async.demo2;

import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.AsyncResult;
import org.springframework.stereotype.Component;

import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

@Async
@Component
public class GoodsService {
    //Simulate to obtain the basic information of the product, the internal time is 500 milliseconds
    public Future<String> getGoodsInfo(long goodsId) throws InterruptedException {
        TimeUnit. MILLISECONDS. sleep(500);
        return AsyncResult.forValue(String.format("basic information of product %s!", goodsId));
    }

    //Simulate obtaining product description information, the internal time is 500 milliseconds
    public Future<String> getGoodsDesc(long goodsId) throws InterruptedException {
        TimeUnit. MILLISECONDS. sleep(500);
        return AsyncResult.forValue(String.format("Description information of product %s!", goodsId));
    }

    //Simulate obtaining the product review information list, which takes 500 milliseconds internally
    public Future<List<String>> getGoodsComments(long goodsId) throws InterruptedException {
        TimeUnit. MILLISECONDS. sleep(500);
        List<String> comments = Arrays.asList("Comment 1", "Comment 2");
        return AsyncResult.forValue(comments);
    }
}

Come to a spring configuration class, you need to add @EnableAsync to open the asynchronous call of the bean method.

package com.javacode2018.async.demo2;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.scheduling.annotation.EnableAsync;

@ComponentScan
@EnableAsync
public class MainConfig2 {
}

test code

@Test
public void test2() throws InterruptedException, ExecutionException {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
    context.register(MainConfig2.class);
    context. refresh();
    GoodsService goodsService = context. getBean(GoodsService. class);

    long starTime = System. currentTimeMillis();
    System.out.println("Start to obtain various information of the product");

    long goodsId = 1L;
    Future<String> goodsInfoFuture = goodsService.getGoodsInfo(goodsId);
    Future<String> goodsDescFuture = goodsService.getGoodsDesc(goodsId);
    Future<List<String>> goodsCommentsFuture = goodsService.getGoodsComments(goodsId);

    System.out.println(goodsInfoFuture.get());
    System.out.println(goodsDescFuture.get());
    System.out.println(goodsCommentsFuture.get());

    System.out.println("The product information is obtained, the total time (ms): " + (System.currentTimeMillis() - starTime));

    //Sleep to prevent @Test from exiting
    TimeUnit. SECONDS. sleep(3);
}

run output

Start to obtain various information about the product
Commodity 1 basic information!
Commodity 1 description information!
[Comment 1, Comment 2]
The product information is obtained, the total time (ms): 525

The three methods take a total of about 500 milliseconds.

If the asynchronous method is not used, the three methods will be executed synchronously, which takes about 1.5 seconds. Let’s try it, remove @Async from GoodsService, and then execute the test again case, output

Start to obtain various information about the product
Commodity 1 basic information!
Commodity 1 description information!
[Comment 1, Comment 2]
The product information is obtained, the total time spent (ms): 1503

You can learn from this case. According to this idea, you can optimize your code. If there is no connection between methods, you can use an asynchronous method to obtain them in parallel. In the end, the method that takes the longest time will be compared to the overall method. The performance of the synchronization method has been improved a lot.

6. Customize the thread pool for asynchronous execution

By default, @EnableAsync uses the built-in thread pool to call methods asynchronously, but we can also customize the thread pool for asynchronously executing tasks.

There are 2 ways to customize the thread pool for asynchronous processing

way 1

Define a bean of thread pool type in the spring container, the bean name must be taskExecutor

@Bean
public Executor taskExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(10);
    executor.setMaxPoolSize(100);
    executor.setThreadNamePrefix("my-thread-");
    return executor;
}

way 2

Define a bean and implement the getAsyncExecutor method in the AsyncConfigurer interface. This method needs to return a custom thread pool. Example code:

package com.javacode2018.async.demo3;

import com.javacode2018.async.demo1.LogService;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.lang.Nullable;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;

@EnableAsync
public class MainConfig3 {

    @Bean
    public LogService logService() {
        return new LogService();
    }

    /**
     * Define a bean of type AsyncConfigurer, implement the getAsyncExecutor method, and return a custom thread pool
     *
     * @param executor
     * @return
     */
    @Bean
    public AsyncConfigurer asyncConfigurer(@Qualifier("logExecutors") Executor executor) {
        return new AsyncConfigurer() {
            @Nullable
            @Override
            public Executor getAsyncExecutor() {
                return executor;
            }
        };
    }

    /**
     * Define a thread pool for asynchronously processing log method calls
     *
     * @return
     */
    @Bean
    public Executor logExecutors() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10);
        executor.setMaxPoolSize(100);
        // thread name prefix
        executor.setThreadNamePrefix("log-thread-"); //@1
        return executor;
    }

}

@1The prefix of the thread name in the custom thread pool is log-thread-, run the following test code

@Test
public void test3() throws InterruptedException {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
    context.register(MainConfig3.class);
    context. refresh();
    LogService logService = context. getBean(LogService. class);
    System.out.println(Thread.currentThread() + "logService.log start," + System.currentTimeMillis());
    logService.log("Asynchronous execution method!");
    System.out.println(Thread.currentThread() + "logService.log end," + System.currentTimeMillis());

    //Sleep to prevent @Test from exiting
    TimeUnit. SECONDS. sleep(3);
}

output

Thread[main,5,main] logService.log start,1595228732914
Thread[main,5,main] logService.log end,1595228732921
Thread[log-thread-1,5,main] starts logging, 1595228732930
Thread[log-thread-1,5,main] logging completed, 1595228734931

The thread name in the last two lines of logs is log-thread-, which is the thread in our custom thread pool.

7. Custom exception handling

If an exception occurs in an asynchronous method, how do we get the exception information? At this time, it can be solved by custom exception handling.

There are two types of exception handling

  1. When the return value is Future, when there is an exception inside the method, the exception will be thrown out, you can use try..catch for Future.get to catch the exception
  2. When the return value is not Future, you can customize a bean, implement the getAsyncUncaughtExceptionHandler method in the AsyncConfigurer interface, and return a custom exception handler

Case 1: The return value is Future type

usage

Exceptions are caught by try..catch, as follows

try {
    Future<String> future = logService. mockException();
    System.out.println(future.get());
} catch (ExecutionException e) {
    System.out.println("Catch ExecutionException");
    //Get the actual exception information through e.getCause
    e.getCause().printStackTrace();
} catch (InterruptedException e) {
    e.printStackTrace();
}

the case

Add a method to LogService, the return value is Future, and an exception is thrown internally, as follows:

@Async
public Future<String> mockException() {
    // Simulate throwing an exception
    throw new IllegalArgumentException("The parameter is wrong!");
}

The test code is as follows

@Test
public void test5() throws InterruptedException {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
    context.register(MainConfig1.class);
    context. refresh();
    LogService logService = context. getBean(LogService. class);
    try {
        Future<String> future = logService. mockException();
        System.out.println(future.get());
    } catch (ExecutionException e) {
        System.out.println("Catch ExecutionException");
        //Get the actual exception information through e.getCause
        e.getCause().printStackTrace();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    //Sleep to prevent @Test from exiting
    TimeUnit. SECONDS. sleep(3);
}

run output

java.lang.IllegalArgumentException: Argument error!
Catch ExecutionException
 at com.javacode2018.async.demo1.LogService.mockException(LogService.java:23)
 at com.javacode2018.async.demo1.LogService$$FastClassBySpringCGLIB$$32a28430.invoke(<generated>)
 at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)

Case 2: No return value exception handling

usage

When the return value is not a Future, you can customize a bean, implement the getAsyncUncaughtExceptionHandler method in the AsyncConfigurer interface, and return a custom exception handler. When the target method throws an exception during execution, this will automatically call back the AsyncUncaughtExceptionHandler#handleUncaughtException method, and you can handle exceptions in this method, as follows:

@Bean
public AsyncConfigurer asyncConfigurer() {
    return new AsyncConfigurer() {
        @Nullable
        @Override
        public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
            return new AsyncUncaughtExceptionHandler() {
                @Override
                public void handleUncaughtException(Throwable ex, Method method, Object... params) {
                    //When an exception is thrown during the execution of the target method, this method will be automatically called back at this time, and the exception can be handled in this method
                }
            };
        }
    };
}

the case

Add a method to LogService that throws an exception internally, as follows:

@Async
public void mockNoReturnException() {
    // Simulate throwing an exception
    throw new IllegalArgumentException("Exception with no return value!");
}

Come to a spring configuration class to customize the exception handler AsyncUncaughtExceptionHandler through AsyncConfigurer

package com.javacode2018.async.demo4;

import com.javacode2018.async.demo1.LogService;
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.lang.Nullable;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;

import java.lang.reflect.Method;
import java.util.Arrays;

@EnableAsync
public class MainConfig4 {

    @Bean
    public LogService logService() {
        return new LogService();
    }

    @Bean
    public AsyncConfigurer asyncConfigurer() {
        return new AsyncConfigurer() {
            @Nullable
            @Override
            public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
                return new AsyncUncaughtExceptionHandler() {
                    @Override
                    public void handleUncaughtException(Throwable ex, Method method, Object... params) {
                        String msg = String.format("Method [%s], parameter [%s], an exception has been sent, and the details of the exception:", method, Arrays.asList(params));
                        System.out.println(msg);
                        ex. printStackTrace();
                    }
                };
            }
        };
    }

}

run output

Method [public void com.javacode2018.async.demo1.LogService.mockNoReturnException()], parameter [[]], sending exception, exception details:
java.lang.IllegalArgumentException: No return value exception!
 at com.javacode2018.async.demo1.LogService.mockNoReturnException(LogService.java:29)
 at com.javacode2018.async.demo1.LogService$$FastClassBySpringCGLIB$$32a28430.invoke(<generated>)
 at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)

8. Thread pool isolation

What is thread pool isolation?

There may be many businesses in a system, such as recharge service, cash withdrawal service or other services. Some methods in these services need to be executed asynchronously. By default, they will use the same thread pool to execute. If there is a business with a relatively large volume, Occupying a large number of threads in the thread pool will cause other business methods to fail to execute. Then we can use thread isolation to use different thread pools for different businesses, which are isolated from each other and do not affect each other.

The @Async annotation has a value parameter, which is used to specify the bean name of the thread pool. When the method is running, the specified thread pool will be used to execute the target method.

How to use

  1. In the spring container, customize the bean related to the thread pool
  2. @Async(“thread pool bean name”)

Case

Simulate two businesses: asynchronous recharge and asynchronous withdrawal; both businesses use independent thread pools to execute asynchronously without affecting each other.

Asynchronous recharge service

package com.javacode2018.async.demo5;

import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

@Component
public class RechargeService {
    //Simulate asynchronous recharge
    @Async(MainConfig5. RECHARGE_EXECUTORS_BEAN_NAME)
    public void recharge() {
        System.out.println(Thread.currentThread() + "Simulated asynchronous recharge");
    }
} 

Asynchronous withdrawal service

package com.javacode2018.async.demo5;

import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

@Component
public class CashOutService {
    //Simulate asynchronous withdrawal
    @Async(MainConfig5. CASHOUT_EXECUTORS_BEAN_NAME)
    public void cashOut() {
        System.out.println(Thread.currentThread() + "Simulate asynchronous withdrawal");
    }
}

spring configuration class

Note that the codes of @0, @1, @2, @3, @4 adopt thread pool isolation and register 2 thread pools to process the above 2 respectively. an asynchronous business.

package com.javacode2018.async.demo5;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;

@EnableAsync //@0: Enable method asynchronous invocation
@ComponentScan
public class MainConfig5 {

    //@1: Value business thread pool bean name
    public static final String RECHARGE_EXECUTORS_BEAN_NAME = "rechargeExecutors";
    //@2: Withdrawal business thread pool bean name
    public static final String CASHOUT_EXECUTORS_BEAN_NAME = "cashOutExecutors";

    /**
     * @3: The recharge thread pool, the thread name starts with recharge-thread-
     * @return
     */
    @Bean(RECHARGE_EXECUTORS_BEAN_NAME)
    public Executor rechargeExecutors() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10);
        executor.setMaxPoolSize(100);
        // thread name prefix
        executor.setThreadNamePrefix("recharge-thread-");
        return executor;
    }

    /**
     * @4: The thread pool for recharging, the thread name starts with cashOut-thread-
     *
     * @return
     */
    @Bean(CASHOUT_EXECUTORS_BEAN_NAME)
    public Executor cashOutExecutors() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10);
        executor.setMaxPoolSize(100);
        // thread name prefix
        executor.setThreadNamePrefix("cashOut-thread-");
        return executor;
    }
}

test code

@Test
public void test7() throws InterruptedException {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
    context.register(MainConfig5.class);
    context. refresh();

    RechargeService rechargeService = context. getBean(RechargeService. class);
    rechargeService. recharge();
    CashOutService cashOutService = context. getBean(CashOutService. class);
    cashOutService. cashOut();

    //Sleep to prevent @Test from exiting
    TimeUnit. SECONDS. sleep(3);
}

run output

Thread[recharge-thread-1,5,main] simulates asynchronous recharge
Thread[cashOut-thread-1,5,main] simulates asynchronous cash withdrawal

From the output, it can be seen that the two services are executed using different thread pools.

9. Source code & amp; principle

Implemented internally using aop, @EnableAsync will introduce a bean post-processor: AsyncAnnotationBeanPostProcessor, and register it in the spring container. This bean post-processor judges the bean class during all bean creation processes Is there an @Async annotation on it or is there a method marked with @Async in the class? If so, a proxy object will be generated for this bean through aop, and an aspect will be added to the proxy object: org.springframework.scheduling.annotation.AsyncAnnotationAdvisor, this An interceptor will be introduced in the aspect: AnnotationAsyncExecutionInterceptor. The key code for method asynchronous invocation is implemented in the invoke method of this interceptor. You can take a look.

10. Summary

https://gitee.com/javacode2018/spring-series

Passerby A java all case codes will be put on this in the future, everyone watch it, you can continue to pay attention to the dynamics.

Source: https://mp.weixin.qq.com/s?__biz=MzA5MTkxMDQ4MQ== &mid=2648935642 &idx=2 &sn=6b9ac2b42f5c5da424a424ec909392fe &scene=21#wechat_redirect