Spring asynchronous call, multithreading, one line code implementation

Posted by kumschick on Tue, 09 Jun 2020 08:13:32 +0200

Spring asynchronous call, multithreading

  • summary
  • quick get start
  • Asynchronous callback
  • Asynchronous exception handling
  • Custom actuator

1. Overview

In daily development, our logic is synchronous calling and sequential execution. But in some cases, we want to call asynchronously to separate the main thread and part of the logic, so as to achieve faster program execution and improve performance. For example, highly concurrent interfaces, user operation logs, etc.

Asynchronous call, corresponding to synchronous call.
Synchronous call: a program is executed in sequence according to the defined order, and each line of program must wait for the execution of the previous line of program before it can be executed; asynchronous call: when a program is executed in sequence, the subsequent program is executed without waiting for the asynchronous call to return the execution result.

Considering the reliability of asynchrony, we will generally consider introducing message queues, such as RabbitMQ, RocketMQ, Kafka, etc. But in some cases, we don't need such high reliability. We can use in-process queues or thread pools.

public static void main(String[] args) {
        // Create a thread pool. This is just a temporary test. Don't deduct the software from complying with the Alibaba Java development specification, YEAH
        ExecutorService executor = Executors.newFixedThreadPool(10);

        // Submit tasks to the thread pool for execution.
        executor.submit(new Runnable() {

            @Override
            public void run() {
                System.out.println("I heard that I was called asynchronously");
            }

        });
    }
The relatively unreliable reason for the queue or thread pool in the process is that the tasks in the queue and thread pool are only stored in memory. How to shut down the JVM process abnormally will result in loss and not execution.
In distributed message queue, asynchronous call will be stored on the message server in the form of a message, so even if the JVM process is interrupted abnormally, the message will still be on the server of message service queue
So if you use the queue or thread pool in the process to implement asynchronous calls, you must ensure the graceful shutdown of the JVM process as much as possible to ensure that they are executed before the shutdown.

In the Spring Task module of the Spring Framework, @ Async annotation is provided, which can be added to a method to automatically implement the asynchronous call of the method

😈 In short, we can use @ Transactional declarative transaction and @ Async annotation provided by Spring Task, 😈 Declarative asynchronous. On the principle of implementation, it is also based on Spring AOP interception to implement asynchronous submission of the operation to the thread pool to achieve the purpose of asynchronous call.

2. Getting started

2.1 introduce dependency

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.1.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>lab-29-async-demo</artifactId>

    <dependencies>
        <!-- introduce Spring Boot rely on -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <!-- Easy to write unit tests later -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

</project>

Copy code

Because Spring Task is a module of Spring Framework, we do not need to introduce it specially after we introduce spring boot web dependency.

2.2 Application

Create Application class and add @ EnableAsync to enable @ Async support

@SpringBootApplication
@EnableAsync // Enable @ Async support
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
  • Add @ EnableAsync annotation on the class to enable asynchronous function.

2.3 DemoService

package cn.iocoder.springboot.lab29.asynctask.service;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.AsyncResult;
import org.springframework.stereotype.Service;
import org.springframework.util.concurrent.ListenableFuture;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;

@Service
public class DemoService {

    private Logger logger = LoggerFactory.getLogger(getClass());
    
    public Integer execute01() {
        logger.info("[execute01]");
        sleep(10);
        return 1;
    }

    public Integer execute02() {
        logger.info("[execute02]");
        sleep(5);
        return 2;
    }

    private static void sleep(int seconds) {
        try {
            Thread.sleep(seconds * 1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

    @Async
    public Integer zhaoDaoNvPengYou(Integer a, Integer b) {
        throw new RuntimeException("Programmers don't need girlfriends");
    }

}
  • The execute01 and execute02 methods are defined to simulate sleep for 10 seconds and 5 seconds respectively.
  • At the same time, in the method, use the logger to print the log, which is convenient for us to see the execution time and thread of each method

2.4 synchronous call test

Write DemoServiceTest test test class, add ා task01() method, and call the above methods synchronously. The code is as follows:

@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
public class DemoServiceTest {

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    private DemoService demoService;

    @Test
    public void task01() {
        long now = System.currentTimeMillis();
        logger.info("[task01][Start execution]");

        demoService.execute01();
        demoService.execute02();

        logger.info("[task01][End execution, elapsed time {} millisecond]", System.currentTimeMillis() - now);
    }
}

Run the unit test and print the log as follows:

2020-06-02 09:16:03.391  INFO 3108 --- [      main] c.i.s.l.a.service.DemoServiceTest        : [task01][Start execution]
2020-06-02 09:16:03.402  INFO 3108 --- [      main] c.i.s.l.asynctask.service.DemoService    : [execute01]
2020-06-02 09:16:13.403  INFO 3108 --- [      main] c.i.s.l.asynctask.service.DemoService    : [execute02]
2020-06-02 09:16:18.403  INFO 3108 --- [      main] c.i.s.l.a.service.DemoServiceTest        : [task01][End execution, consuming 15012 MS]
  • Both methods are executed in sequence for 15 seconds.
  • Are executed in the main thread.

2.5 asynchronous call test

Modify DemoServiceTest, add execute01Async() and execute02Async() asynchronous call methods, code:

	@Async
    public Integer execute01Async() {
        return this.execute01();
    }

    @Async
    public Integer execute02Async() {
        return this.execute02();
    }
  • On execute01Async() and execute01Async(), add @ Async to implement asynchronous call

Modify DemoServiceTest class, write ා task02() method, and call the above two methods asynchronously.

	@Test
    public void task02() {
        long now = System.currentTimeMillis();
        logger.info("[task02][Start execution]");

        demoService.execute01Async();
        demoService.execute02Async();

        logger.info("[task02][End execution, elapsed time {} millisecond]", System.currentTimeMillis() - now);
    }

Print log:

2020-06-02 10:57:41.643  INFO 14416 --- [main] c.i.s.l.a.service.DemoServiceTest        : [task02][Start execution]
2020-06-02 10:57:41.675  INFO 14416 --- [main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
2020-06-02 10:57:41.682  INFO 14416 --- [main] c.i.s.l.a.service.DemoServiceTest        : [task02][End execution, taking 39 MS]
  • DemoService's two methods, asynchronous execution, so the main thread only takes about 39 milliseconds. Note that the actual two methods are not implemented.
  • Both methods of DemoService are executed in the asynchronous thread pool.

2.6 wait for asynchronous call to complete the test

In the above * * [2.5 asynchronous call test] * * asynchronous call, the two methods are only asynchronous calls, and the method is not completed. In some business scenarios, we achieve the effect of asynchronous call. At the same time, if the main thread has returned results, we need the main thread to block and wait for the results of asynchronous call.

Modify DemoService, add execute01aasyncwithfuture() and execute01aasyncwithfuture() asynchronous calls, and return the Future object. code:

	@Async
    public Future<Integer> execute01AsyncWithFuture() {
        return AsyncResult.forValue(this.execute01());
    }

    @Async
    public Future<Integer> execute02AsyncWithFuture() {
        return AsyncResult.forValue(this.execute02());
    }
  • In these two asynchronous methods, the AsyncResult.forValue(this.execute02());, return Future object with execution result

Modify DemoServiceTest class, write ා task02() method, call the above two methods asynchronously, and block the thread to wait for the asynchronous call return result

code:

	@Test
    public void task03() throws ExecutionException, InterruptedException {
        long now = System.currentTimeMillis();
        logger.info("[task03][Start execution]");

        // Perform task
        Future<Integer> execute01Result = demoService.execute01AsyncWithFuture();
        Future<Integer> execute02Result = demoService.execute02AsyncWithFuture();
        // Blocking wait results
        execute01Result.get();
        execute02Result.get();

        logger.info("[task03][End execution, elapsed time {} millisecond]", System.currentTimeMillis() - now);
    }
  • Call two methods asynchronously and return the corresponding Future object. These two asynchronous call logic can be executed in parallel.
  • get() method of the Future object, effect: block the thread and wait for the return result.

Print log:

2020-06-02 13:56:43.955  INFO 7828 --- [ main] c.i.s.l.a.service.DemoServiceTest        : [task03][Start execution]
2020-06-02 13:56:43.987  INFO 7828 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
2020-06-02 13:56:44.008  INFO 7828 --- [ task-1] c.i.s.l.asynctask.service.DemoService    : [execute01]
2020-06-02 13:56:44.008  INFO 7828 --- [ task-2] c.i.s.l.asynctask.service.DemoService    : [execute02]
2020-06-02 13:56:54.008  INFO 7828 --- [ main] c.i.s.l.a.service.DemoServiceTest        : [task03][End execution, consuming 10053 MS]
  • Two asynchronous call methods are executed by the thread pool task-1 and task-2 at the same time. Because the main thread blocks and waits for the execution result, the execution time is 10 seconds. When there are multiple asynchronous calls at the same time, the thread blocks and waits, and the execution time is determined by the asynchronous call logic that consumes the longest.

2.7 application profile

In application, add the spring Task configuration

spring:
  task:
    # Spring executor configuration, corresponding to TaskExecutionProperties configuration class. For spring asynchronous tasks, this executor is used.
    execution:
      thread-name-prefix: task- # Prefix for the thread name of the thread pool. The default is task -. It is recommended to set it according to your own application
      pool: # Thread pool related
        core-size: 8 # The number of core threads, the number of threads initialized when the thread pool is created. The default is 8.
        max-size: 20 # The maximum number of threads, the maximum number of threads in the thread pool. Only after the buffer queue is full, can the threads that exceed the number of core threads be applied. Default is Integer.MAX_VALUE
        keep-alive: 60s # Allow the idle time of the thread. When the idle time exceeds that of the thread outside the core thread, it will be destroyed after the idle time arrives. Default is 60 seconds
        queue-capacity: 200 # Buffer queue size, the size of the queue used to buffer the execution of tasks. Default is Integer.MAX_VALUE . 
        allow-core-thread-timeout: true # Whether to allow core thread timeout, that is, to enable dynamic growth and reduction of thread pool. The default is true.
      shutdown:
        await-termination: true # Whether to wait for the scheduled task to complete when the application is closed. The default value is false, and it is recommended to set it to true
        await-termination-period: 60 # The maximum time, in seconds, to wait for a task to complete. The default is 0, set according to your own application
  • Spring itself relies on Spring Task
  • stay spring.task.execution Configuration item, configuration of Spring Task scheduling task, corresponding to TaskExecutionProperties configuration class
  • The Spring Boot TaskExecutionAutoConfiguration class implements the automatic configuration of Spring Task and creates the ThreadPoolTaskExecutor based on the thread pool. In fact, ThreadPoolTaskExecutor is the sub assembly of ThreadPoolExecutor, which mainly adds the execution task and returns the ListenableFuture object function.

As mentioned earlier, asynchronous reliability requires elegant process shutdown. spring.task.execution.shutdown configuration shutdown is to realize the graceful shutdown of Spring Task. During the execution of an asynchronous task, if the application starts to shut down, the beans needed by the asynchronous task are destroyed. For example, if the database connection pool needs to be accessed, the asynchronous task is still executing. Once the database needs to be accessed, but there is no corresponding Bean, an error will be reported.

  • By configuring await termination: true, when the application is closed, wait for the asynchronous task to complete. In this way, when the application is closed, Spring will wait for the ThreadPoolTaskExecutor to finish executing the task, and then destroy the Bean.
  • When the application is closed, in some business scenarios, it is impossible for Spring to wait all the time for the asynchronous task to complete. By configuring await termination period: 60, set the maximum waiting time of Spring. Once the time is up, you will not wait for the asynchronous task to complete.

3. Asynchronous callback

In a business scenario, a callback may be required after an asynchronous task is executed. The following describes the implementation of custom callback after asynchronous execution.

3.1. AsyncResult source code interpretation

In 2.6 waiting for the asynchronous call to complete, we see that the AsyncResult class represents the asynchronous result. The returned results are divided into two situations:

  • When the execution is successful, call the async result ᦇ forvalue (V value) static method to return the successful ListenableFuture object, source code: / * * * create a new async result which exposes the given value from {@ link future ᦇ get()}. * @ param value the value to expose * @ since 4.2 * @ see future ᦇ get() * / public static < V > ListenableFuture < V > forvalue (V value) {return new Asyncresult < > (value, null);} copy code
  • When executing an exception, the asyncresult ා forexecutionexception (throwable Ex) static method is called to return the ListenableFuture object of the exception. Source code: / * * * create a new async result which exposes the given exception as an * {@ link executionexception} from {@ link future ා get()}. * @ param ex the exception to expose (other an pre build {@ link executionexception} * or a cause to be wrapped in an {@ link executionexception}) * @ since 4.2 * @ see executionexception * / public static < V > ListenableFuture < V > for executionexception (throwable Ex) {return new asyncresult < > (null, ex);} copy code

AsyncResult also implements the ListenableFuture interface, which provides asynchronous result callback processing.

public class AsyncResult<V> implements ListenableFuture<V>

ListenableFuture interface, source code:

public interface ListenableFuture<T> extends Future<T> {

    // Add callback method to handle success and exception uniformly.
	void addCallback(ListenableFutureCallback<? super T> callback);

	// Add success and failure callback methods to handle success and exception situations respectively.
	void addCallback(SuccessCallback<? super T> successCallback, FailureCallback failureCallback);


	// Convert ListenableFuture to completable future provided by JDK8.
    // In this way, we can use ListenableFuture to set the callback later
	default CompletableFuture<T> completable() {
		CompletableFuture<T> completable = new DelegatingCompletableFuture<>(this);
		addCallback(completable::complete, completable::completeExceptionally);
		return completable;
	}

}

ListenableFuture inherits Future, so AsyncResult also implements the interface of Future. Source code:

public interface Future<V> {

    // If the task has not started, executing the cancel(...) method will return false;
    // If the task has been started, execute cancel(true) method to try to stop the task by interrupting the thread executing the task. If the stop is successful, return true;
    // When the task has been started, executing the cancel(false) method will not affect the executing task thread (let the thread execute normally to completion), and then return false;
    // When the task is complete, executing the cancel(...) method returns false.
    // The mayInterruptRunning parameter indicates whether to interrupt a thread in execution.
    boolean cancel(boolean mayInterruptIfRunning);

    // Returns true if the task is cancelled before it is completed.
    boolean isCancelled();

    // If the execution of the task is finished, whether it is normal or canceled midway or an exception occurs, it returns true.
    boolean isDone();

    // Gets the result of the asynchronous execution. If no result is available, the method blocks until the asynchronous calculation is completed.
    V get() throws InterruptedException, ExecutionException;
    
	// Get the asynchronous execution result. If no result is available, the method will block, but there will be a time limit. If the blocking time exceeds the set timeout time, the method will throw an exception.
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

The implementation of addCallback(...) method callback in AsyncResult, source code:

	@Override
	public void addCallback(ListenableFutureCallback<? super V> callback) {
		addCallback(callback, callback);
	}

	@Override
	public void addCallback(SuccessCallback<? super V> successCallback, FailureCallback failureCallback) {
		try {
			if (this.executionException != null) { // <1>
				failureCallback.onFailure(exposedException(this.executionException));
			}
			else { // <2>
				successCallback.onSuccess(this.value);
			}
		}
		catch (Throwable ex) { // <3>
			// Ignore
		}
	}

// From the ExecutionException, get the original exception.
private static Throwable exposedException(Throwable original) {
    if (original instanceof ExecutionException) {
        Throwable cause = original.getCause();
        if (cause != null) {
            return cause;
        }
    }
    return original;
}
  • It is known from ListenableFutureCallback that ListenableFutureCallback interface inherits both SuccessCallback and FailureCallback interfaces
public interface ListenableFutureCallback<T> extends SuccessCallback<T>, FailureCallback 
  • <1> If it is an exception handling result, call failureCallback
  • <2> , call the successCallback callback if the result is processed successfully
  • <3> , if the callback logic is abnormal, ignore it directly. Suppose there are multiple callbacks, and one of them has an agenda, which will not affect other callbacks.

In fact, AsyncResult is the result of asynchronous execution. Since it is the result, the execution has been completed. Therefore, when we call the ා addCallback(...) interface method to add a callback, we must directly use the callback to process the result of execution.

All methods defined by AsyncResult for Future are implemented as follows:

// AsyncResult.java

@Override
public boolean cancel(boolean mayInterruptIfRunning) {
    return false; // Because AsyncResult is the result of execution, a direct return of false indicates cancellation failure.
}

@Override
public boolean isCancelled() {
    return false; // Because AsyncResult is the result of execution, a direct return of false indicates that it has not been cancelled.
}

@Override
public boolean isDone() {
    return true; // Because AsyncResult is the result of execution, a direct return of true indicates completion.
}

@Override
@Nullable
public V get() throws ExecutionException {
    // If an exception occurs, it is thrown.
    if (this.executionException != null) {
        throw (this.executionException instanceof ExecutionException ?
                (ExecutionException) this.executionException :
                new ExecutionException(this.executionException));
    }
    // If the execution is successful, the value result will be returned
    return this.value;
}

@Override
@Nullable
public V get(long timeout, TimeUnit unit) throws ExecutionException {
    return get();
}

3.2 ListenableFutureTask

When we call a method annotated with @ Async, if the type returned by the method is ListenableFuture, the actual method returns the ListenableFutureTask object.

The ListenableFutureTask class also implements the ListenableFuture interface, inherits the FutureTask class and the FutureTask implementation class of ListenableFuture.

The "addCallback(...) method defined by ListenableFutureTask for ListenableFuture is as follows:

private final ListenableFutureCallbackRegistry<T> callbacks = new ListenableFutureCallbackRegistry<T>();

@Override
public void addCallback(ListenableFutureCallback<? super T> callback) {
    this.callbacks.addCallback(callback);
}

@Override
public void addCallback(SuccessCallback<? super T> successCallback, FailureCallback failureCallback) {
    this.callbacks.addSuccessCallback(successCallback);
    this.callbacks.addFailureCallback(failureCallback);
}
  • You can see that in the ListenableFutureTask, the staging callback goes to the ListenableFutureCallbackRegistry

ListenableFutureTask rewrites the ා done() method implemented by FutureTask. The implementation source code is as follows:

@Override
	protected void done() {
		Throwable cause;
		try {
            // Get execution results
			T result = get();
            // Execute successful callback
			this.callbacks.success(result);
			return;
		}
		catch (InterruptedException ex) { // If there is an interrupt exception InterruptedException exception, interrupt the current thread and return directly
			Thread.currentThread().interrupt();
			return;
		}
		catch (ExecutionException ex) { // If there is an ExecutionException exception, get the real exception and set it to cause
			cause = ex.getCause();
			if (cause == null) {
				cause = ex;
			}
		}
		catch (Throwable ex) {
			cause = ex; // Set exception to cause
		}
         // Execute exception, execute exception callback
		this.callbacks.failure(cause);
	}

3.3 specific examples

Modify DemoService code, add asynchronous call of ා execute02(), and return ListenableFuture object. The code is as follows:

@Async
    public ListenableFuture<Integer> execute01AsyncWithListenableFuture() {
        try {
            //int i = 1 / 0;
            return AsyncResult.forValue(this.execute02());
        } catch (Throwable ex) {
            return AsyncResult.forExecutionException(ex);
        }
    }
  • Based on the result of execution, wrap the AsyncResult object that is successful or abnormal.

DemoServiceTest test class, write ා task04() method, call the above method asynchronously, add the corresponding Callback method while the plug is waiting for execution to complete. code:

@Test
    public void task04() throws ExecutionException, InterruptedException {
        long now = System.currentTimeMillis();
        logger.info("[task04][Start execution]");

        // <1> Carry out the task
        ListenableFuture<Integer> execute01Result = demoService.execute01AsyncWithListenableFuture();
        logger.info("[task04][execute01Result The types of are:({})]",execute01Result.getClass().getSimpleName());
        execute01Result.addCallback(new SuccessCallback<Integer>() { // < 2.1 > Add successful callback

            @Override
            public void onSuccess(Integer result) {
                logger.info("[onSuccess][result: {}]", result);
            }

        }, new FailureCallback() { // < 2.1 > Add failed callback

            @Override
            public void onFailure(Throwable ex) {
                logger.info("[onFailure][exception occurred]", ex);
            }

        });
        execute01Result.addCallback(new ListenableFutureCallback<Integer>() { // < 2.2 > Add unified callback of success and failure

            @Override
            public void onSuccess(Integer result) {
                logger.info("[onSuccess][result: {}]", result);
            }

            @Override
            public void onFailure(Throwable ex) {
                logger.info("[onFailure][exception occurred]", ex);
            }

        });
        // <3> Blocking wait results
        execute01Result.get();

        logger.info("[task04][End execution, elapsed time {} millisecond]", System.currentTimeMillis() - now);
    }
  • <1> , call the demoservice ා execute01asyncwithlistenablefuture() method, call the method asynchronously, and return the ListenableFutureTask object. Here, let's look at the printed log. 2020-06-08 14:13:16.738 INFO 5060 --- [ main] c.i.s.l.a.service.DemoServiceTest : [task04][execute01Result is of type: (ListenableFutureTask)] copy code
  • For < 2.1 >, add successful callback and failed callback.
  • For < 2.2 >, add the unified callback of success and failure.
  • <3> , blocking waiting results. After execution, we will see that the callback i s executed and the print log is as follows: 2020-06-08 14:13:21.752 info 5060 -- [main] c.i.s.l.a service.DemoServiceTest : [task04] [end execution, 5057ms] 2020-06-08 14:13:21.752 info 5060 -- [TASK-1] c.i.s.l.a service.DemoServiceTest : [onSuccess][result: 2] 2020-06-08 14:13:21.752 INFO 5060 --- [ task-1] c.i.s.l.a. service.DemoServiceTest : [onsuccess] [result: 2] copy code

4. Asynchronous exception handler

By implementing the asyncuncautexceptionhandler interface, the exception of asynchronous call can be handled uniformly.

Create the GlobalAsyncExceptionHandler class, which is a globally unified asynchronous call exception handler. code:

@Component
public class GlobalAsyncExceptionHandler implements AsyncUncaughtExceptionHandler {

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Override
    public void handleUncaughtException(Throwable ex, Method method, Object... params) {
        logger.error("[handleUncaughtException][method({}) params({}) exception occurred]",
                method, params, ex);
    }

}
  • On class, we added @ Component annotation, considering that fat friends may inject some spring beans into properties.
  • Implement the method of "handle uncaughtexception" (throwable ex, method method, object... Params), and print the exception log.

Note that asyncuncautexceptionhandler can only intercept asynchronous call methods with return types other than Future. By looking at the source code of asynexecutionaspectsupport ා handleerror (throwable ex, method method, object... Params), you can easily get this conclusion. Code:

// AsyncExecutionAspectSupport.java

protected void handleError(Throwable ex, Method method, Object... params) throws Exception {
    // a key!!! If the return type is Future, the exception is thrown directly.
    if (Future.class.isAssignableFrom(method.getReturnType())) {
        ReflectionUtils.rethrowException(ex);
    } else {
        // Otherwise, leave it to the AsyncUncaughtExceptionHandler.
        // Could not transmit the exception to the caller with default executor
        try {
            this.exceptionHandler.obtain().handleUncaughtException(ex, method, params);
        } catch (Throwable ex2) {
            logger.warn("Exception handler for async method '" + method.toGenericString() +
                    "' threw unexpected exception itself", ex2);
        }
    }
}
  • By the way, asynexecutionaspectsupport is the parent class of asynexecutioninterceptor. Therefore, the asynchronous call method with return type of Future needs to be processed through "3. Asynchronous callback".

4.2 AsyncConfig

Create the AsyncConfig class and configure the exception handler. code:

@Configuration
@EnableAsync // Enable @ Async support
public class AsyncConfig implements AsyncConfigurer {

    @Autowired
    private GlobalAsyncExceptionHandler exceptionHandler;

    @Override
    public Executor getAsyncExecutor() {
        return null;
    }

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return exceptionHandler;
    }

}
  • Add @ EnableAsync annotation on the class to enable asynchronous function. In this way, the @ EnableAsync annotation of "2. Application" can also be removed.
  • Implement the AsyncConfigurer interface and realize the asynchronous related global configuration. At this moment, fat friend did not expect the WebMvcConfigurer interface of SpringMVC.
  • Implement the getasyncuncaugtexceptionhandler() method to return the GlobalAsyncExceptionHandler object we defined.
  • Implement the getasynexecutor () method to return the default executor of the Spring Task asynchronous task. Here, we return null without defining a default actuator. Therefore, the ThreadPoolTaskExecutor task executor created by TaskExecutionAutoConfiguration automation configuration class will be used as the default executor.

4.3 DemoService

DemoService class, adding asynchronous call to "datanvpengyou (...).". The code is as follows:

@Async
public Integer zhaoDaoNvPengYou(Integer a, Integer b) {
    throw new RuntimeException("Asynchronous global exception");
}

4.4 simple test

 @Test
    public void testZhaoDaoNvPengYou() throws InterruptedException {
        demoService.zhaoDaoNvPengYou(1, 2);

        // Sleep for one second to ensure the execution of asynchronous calls
        Thread.sleep(1000);
    }

Run the unit test, and the execution log is as follows:

2020-06-08 15:26:35.120 ERROR 11388 --- [         task-1] .i.s.l.a.c.a.GlobalAsyncExceptionHandler : [handleUncaughtException][method(public java.lang.Integer cn.iocoder.springboot.lab29.asynctask.service.DemoService.zhaoDaoNvPengYou(java.lang.Integer,java.lang.Integer)) params([1, 2]) exception occurred]

java.lang.RuntimeException: Asynchronous global exception

5. Custom actuator

In the above, we use the Spring Boot TaskExecutionAutoConfiguration automatic configuration class to automatically configure the ThreadPoolTaskExecutor task executor.

In this section, we want to use two custom ThreadPoolTaskExecutor task executors to implement different methods.

5.1 introduce dependency

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.1.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>lab-29-async-demo</artifactId>

    <dependencies>
        <!-- introduce Spring Boot rely on -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <!-- Easy to write unit tests later -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

</project>
Copy code
  • Consistent with the introduction of dependency above.

5.2 application profile

At application.yml Add the configuration of Spring Task timing task as follows:

spring:
  task:
    # Spring executor configuration, corresponding to TaskExecutionProperties configuration class. For spring asynchronous tasks, this executor is used.
    execution-one:
      thread-name-prefix: task-one- # Prefix for the thread name of the thread pool. The default is task -. It is recommended to set it according to your own application
      pool: # Thread pool related
        core-size: 8 # The number of core threads, the number of threads initialized when the thread pool is created. The default is 8.
        max-size: 20 # The maximum number of threads, the maximum number of threads in the thread pool. Only after the buffer queue is full, can the threads that exceed the number of core threads be applied. Default is Integer.MAX_VALUE
        keep-alive: 60s # Allow the idle time of the thread. When the idle time exceeds that of the thread outside the core thread, it will be destroyed after the idle time arrives. Default is 60 seconds
        queue-capacity: 200 # Buffer queue size, the size of the queue used to buffer the execution of tasks. Default is Integer.MAX_VALUE . 
        allow-core-thread-timeout: true # Whether to allow core thread timeout, that is, to enable dynamic growth and reduction of thread pool. The default is true.
      shutdown:
        await-termination: true # Whether to wait for the scheduled task to complete when the application is closed. The default value is false, and it is recommended to set it to true
        await-termination-period: 60 # The maximum time, in seconds, to wait for a task to complete. The default is 0, set according to your own application
    # Spring executor configuration, corresponding to TaskExecutionProperties configuration class. For spring asynchronous tasks, this executor is used.
    execution-two:
      thread-name-prefix: task-two- # Prefix for the thread name of the thread pool. The default is task -. It is recommended to set it according to your own application
      pool: # Thread pool related
        core-size: 8 # The number of core threads, the number of threads initialized when the thread pool is created. The default is 8.
        max-size: 20 # The maximum number of threads, the maximum number of threads in the thread pool. Only after the buffer queue is full, can the threads that exceed the number of core threads be applied. Default is Integer.MAX_VALUE
        keep-alive: 60s # Allow the idle time of the thread. When the idle time exceeds that of the thread outside the core thread, it will be destroyed after the idle time arrives. Default is 60 seconds
        queue-capacity: 200 # Buffer queue size, the size of the queue used to buffer the execution of tasks. Default is Integer.MAX_VALUE . 
        allow-core-thread-timeout: true # Whether to allow core thread timeout, that is, to enable dynamic growth and reduction of thread pool. The default is true.
      shutdown:
        await-termination: true # Whether to wait for the scheduled task to complete when the application is closed. The default value is false, and it is recommended to set it to true
        await-termination-period: 60 # The maximum time, in seconds, to wait for a task to complete. The default is 0, set according to your own application
//Copy code
  • At spring.task Under configuration item, we added the configuration of execution one and execution two executors. In terms of format, we keep and see spring.task.exeuction Consistent. It is convenient for us to reuse the TaskExecutionProperties property configuration class to map.

5.3 AsyncConfig

Create the AsyncConfig class and configure two executors. The code is as follows:

@Configuration
@EnableAsync // Enable @ Async support
public class AsyncConfig
{

    public static final String EXECUTOR_ONE_BEAN_NAME = "executor-one";
    public static final String EXECUTOR_TWO_BEAN_NAME = "executor-two";

    @Configuration
    public static class ExecutorOneConfiguration
    {

        @Bean(name = EXECUTOR_ONE_BEAN_NAME + "-properties")
        @Primary
        @ConfigurationProperties(prefix = "spring.task.execution-one")
        // Read spring.task.execution-one configuration to TaskExecutionProperties object
        public TaskExecutionProperties taskExecutionProperties()
        {
            return new TaskExecutionProperties();
        }

        @Bean(name = EXECUTOR_ONE_BEAN_NAME)
        public ThreadPoolTaskExecutor threadPoolTaskExecutor()
        {
            // Create TaskExecutorBuilder object
            TaskExecutorBuilder builder = createTskExecutorBuilder(this.taskExecutionProperties());
            // Create the ThreadPoolTaskExecutor object
            return builder.build();
        }

    }

    @Configuration
    public static class ExecutorTwoConfiguration
    {

        @Bean(name = EXECUTOR_TWO_BEAN_NAME + "-properties")
        @ConfigurationProperties(prefix = "spring.task.execution-two")
        // Read spring.task.execution-two configuration to TaskExecutionProperties object
        public TaskExecutionProperties taskExecutionProperties()
        {
            return new TaskExecutionProperties();
        }

        @Bean(name = EXECUTOR_TWO_BEAN_NAME)
        public ThreadPoolTaskExecutor threadPoolTaskExecutor()
        {
            // Create TaskExecutorBuilder object
            TaskExecutorBuilder builder = createTskExecutorBuilder(this.taskExecutionProperties());
            // Create ThreadPoolTaskExecutor object
            return builder.build();
        }
    }

    private static TaskExecutorBuilder createTskExecutorBuilder(TaskExecutionProperties properties)
    {
        // Pool properties
        TaskExecutionProperties.Pool pool = properties.getPool();
        TaskExecutorBuilder builder = new TaskExecutorBuilder();
        builder = builder.queueCapacity(pool.getQueueCapacity());
        builder = builder.corePoolSize(pool.getCoreSize());
        builder = builder.maxPoolSize(pool.getMaxSize());
        builder = builder.allowCoreThreadTimeOut(pool.isAllowCoreThreadTimeout());
        builder = builder.keepAlive(pool.getKeepAlive());
        // Shutdown property
        TaskExecutionProperties.Shutdown shutdown = properties.getShutdown();
        builder = builder.awaitTermination(shutdown.isAwaitTermination());
        builder = builder.awaitTerminationPeriod(shutdown.getAwaitTerminationPeriod());
        // Other basic properties
        builder = builder.threadNamePrefix(properties.getThreadNamePrefix());
//        builder = builder.customizers(taskExecutorCustomizers.orderedStream()::iterator);
//        builder = builder.taskDecorator(taskDecorator.getIfUnique());
        return builder;
    }

}
  • Refer to the Spring Boot TaskExecutionAutoConfiguration automatic configuration class. We created ExecutorOneConfiguration and executorttwoconfiguration configuration classes to create two executors named executor one and executor two, respectively.

5.4 DemoService

@Service
public class DemoService
{

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Async(AsyncConfig.EXECUTOR_ONE_BEAN_NAME)
    public Integer execute01()
    {
        logger.info("[execute01]");
        return 1;
    }

    @Async(AsyncConfig.EXECUTOR_TWO_BEAN_NAME)
    public Integer execute02()
    {
        logger.info("[execute02]");
        return 2;
    }

}
  • On the @ Async annotation, we set the Bean name of the executor it uses.

5.5 simple test

@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
public class DemoServiceTest
{

    @Autowired
    private DemoService demoService;

    @Test
    public void testExecute() throws InterruptedException
    {
        demoService.execute01();
        demoService.execute02();

        // Sleep for one second to ensure the execution of asynchronous calls
        Thread.sleep(1000);
    }

}

Run the unit test, and the execution log is as follows:

2020-06-08 15:38:28.846  INFO 12020 --- [     task-one-1] c.i.s.l.asynctask.service.DemoService    : [execute01]
2020-06-08 15:38:28.846  INFO 12020 --- [     task-two-1] c.i.s.l.asynctask.service.DemoService    : [execute02]
  • From the log, we can see that the ා execute01() method is executed in the executor one executor, while the 񖓿 execute02() method is executed in the executor two executor.

Topics: Spring Java Maven Apache