大家好,欢迎来到IT知识分享网。
据我观察,无论是初级新手还是高级开发者都流行用 Spring Boot 构建自己的程序。Spring Boot “约定优于配置”的风格让大家在开发时能专注于业务逻辑。需要的时候,查阅 Spring Boot 教程就可以很方便地了解 Spring 的工作机制。尽管大多数时候只要添加几个注解就可以搞定,但有时候还是需要了解它背后的运行机制,这样才能更专业地使用 Spring Boot。
本文将尝试介绍如何在 Spring 中进行异步处理。
异步处理适用那些与业务逻辑(横切关注点)不直接相关或者不作为其他业务逻辑输入的部分,也可在分布式系统中解耦。
*译注:横切关注点(cross-cutting concerns)指一些具有横越多个模块的行为,使用传统的软件开发方法不能够达到有效模块化的一类特殊关注点。*
Spring 中,`@Async`注解可以标记异步操作。然而,使用`@Async`时有一些限制,仅仅把它加在方法上并不能确保方法会在独立的线程中执行。如果你只是偶尔用到 `@Async`,需要格外当心。
1. @Async 的工作机制
首先为方法添加 `Async` 注解。接着,Spring 会基于 `proxyTargetClass` 属性,为包含 `Async` 定义的对象创建代理(JDK Proxy/CGlib)。最后,Spring 会尝试搜索与当前上下文相关的线程池,把该方法作为独立的执行路径提交。确切地说,Spring 会搜索唯一的 `TaskExecutor` bean 或者名为 `taskExecutor` 的 bean。如果找不到,则使用默认的 `SimpleAsyncTaskExecutor`。
要完成上面的过程,使用中需要注意几个限制,否则会出现 `Async` 不起作用的情况。
2. @Async 的限制
1. 必须在标记 `@ComponentScan` 或 `@configuration` 的类中使用 `@Async`。
2.1 在类中使用 Async 注解
```java package com.example.ask2shamik.springAsync.demo; import java.util.Map; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Component; @Component public class AsyncMailTrigger { @Async public void senMail(Map<String,String> properties) { System.out.println("Trigger mail in a New Thread :: " + Thread.currentThread().getName()); properties.forEach((K,V)->System.out.println("Key::" + K + " Value ::" + V)); } } ```
IT知识分享网
2.2 Caller 类
IT知识分享网```java package com.example.ask2shamik.springAsync.demo; import java.util.HashMap; import java.util.Map; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component public class AsyncCaller { @Autowired AsyncMailTrigger asyncMailTriggerObject; public void rightWayToCall() { System.out.println("Calling From rightWayToCall Thread " + Thread.currentThread().getName()); asyncMailTriggerObject.senMail(populateMap()); } public void wrongWayToCall() { System.out.println("Calling From wrongWayToCall Thread " + Thread.currentThread().getName()); AsyncMailTrigger asyncMailTriggerObject = new AsyncMailTrigger(); asyncMailTriggerObject.senMail(populateMap()); } private Map<String,String> populateMap(){ Map<String,String> mailMap= new HashMap<String,String>(); mailMap.put("body", "A Ask2Shamik Article"); return mailMap; } } ```
上面的例子中,使用了 `@Autowired` 的 `AsyncMailTrigger` 受 `@ComponentScan` 管理,因而会创建新线程执行。而 `WrongWayToCall` 方法中创建的局部对象,不受 `@ComponentScan` 管理,不会创建新线程。
2.3 输出
```shell Calling From rightWayToCall Thread main 2019-03-09 14:08:28.893 INFO 8468 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor' Trigger mail in a New Thread :: task-1 Key::body Value ::A Ask2Shamik Article ++++++++++++++++ Calling From wrongWayToCall Thread main Trigger mail in a New Thread :: main Key::body Value ::A Ask2Shamik Article ```
2. 不要在 `private` 方法上使用 `@Async` 注解。由于在运行时不能创建代理,所以不起作用。
IT知识分享网```java @Async private void senMail() { System.out.println("A proxy on Private method " + Thread.currentThread().getName()); } ```
3. 调用 `methodAsync` 的 `caller` 方法与 `@Async` 方法应该在不同的类中定义。否则,尽管创建了代理对象,但 `caller` 会绕过代理直接调用方法,不会创建新线程。
(https://dzone.com/storage/temp/11422746-springasync.jpg)
2.4 示例
```java package com.example.ask2shamik.springAsync.demo; import java.util.HashMap; import java.util.Map; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Component; @Component public class AsyncCaller { @Autowired AsyncMailTrigger asyncMailTriggerObject; public void rightWayToCall() { System.out.println("Calling From rightWayToCall Thread " + Thread.currentThread().getName()); asyncMailTriggerObject.senMail(populateMap()); } public void wrongWayToCall() { System.out.println("Calling From wrongWayToCall Thread " + Thread.currentThread().getName()); this.senMail(populateMap()); } private Map<String,String> populateMap(){ Map<String,String> mailMap= new HashMap<String,String>(); mailMap.put("body", "A Ask2Shamik Article"); return mailMap; } @Async public void senMail(Map<String,String> properties) { System.out.println("Trigger mail in a New Thread :: " + Thread.currentThread().getName()); properties.forEach((K,V)->System.out.println("Key::" + K + " Value ::" + V)); } } ```
最后,在执行的时候应当使用 `@EnableAsync` 注解。它的作用是让 Spring 在后台线程池中提交 `@Async` 方法。要自定义 `Executor` 可自己实现 bean。在接下来的文章中会给出具体的示例。
```java package com.example.ask2shamik.springAsync; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.scheduling.annotation.EnableAsync; import com.example.ask2shamik.springAsync.demo.AsyncCaller; @SpringBootApplication @EnableAsync public class DemoApplication { @Autowired AsyncCaller caller; public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } @Bean public CommandLineRunner commandLineRunner(ApplicationContext ctx) { return args -> { caller.rightWayToCall(); Thread.sleep(1000); System.out.println("++++++++++++++++"); Thread.sleep(1000); caller.wrongWayToCall(); }; } } ```
Spring Boot 使用 `@Async` 注解时如何捕捉异常。
从主线程 fork 新线程时,有两种情况:
1. “Fire-and-forget”:fork 线程,然后为这个线程分配任务,接下来什么也不用管。不需要关心任务执行结果,其他业务逻辑的执行也不依赖该结果。通常任务的返回类型是 `void`。让我们通过例子帮助理解:假设你在为员工发薪水,需要给每个员工发送一份工资单邮件,你可以异步执行该任务。发邮件显然不是核心业务逻辑,而是一个横切关注点。然而,发邮件很好,而且在某些情况下是必须的。这时候需要制定失败重试或者定时机制。
2. “Fire-with-callback”:在主线程中 fork 一个线程,为该线程分配任务并关联 `Callback`。接下来,主线程会继续执行其他任务并持续检查 `Callback` 结果。主线程需要子线程 `Callback` 执行结果进行下一步工作。
假设你正在做一份员工报告,员工信息根据各自数据类型存储在不同的后端。General Service 存储员工通用数据,比如姓名、生日、性别、地址等;Financial Service 存储薪资、税金以及其他 PF 相关数据。因此,你会创建两个并行线程,分别调用 General Service 与 Financial Service。这两组数据最终都要在报告中体现,因此需要进行数据组合,在主线程中表现为子线程 Callback 结果。一般会用 `CompletebleFuture` 实现。
在上面描述的场景中,如果一切顺利是最理想的结果。但如果执行中发生异常,该如何进行异常处理?
第二种情况下,由于回调执行后能够返回成功或失败,因此处理异常非常容易。失败的时候,异常会被封装在 `CompltebleFuture` 里,在主线程中可以检查异常并处理。处理异常的 Java 代码很简单,这里直接略过。
然而,第一种情况的异常处理非常棘手:创建的线程会执行业务逻辑,但如何确保业务执行成功?或者说,执行失败该如何进行调试,如何追踪是什么地方出现了问题?
解决方案很简单:注入自己的 exception handler。这样,当 `Async` 方法执行过程中发生异常,会把程序控制转交给 handler。你的 handler 知道接下来该如何处理。很简单,不是吗?
要做到这一点,需要执行以下步骤:
1. `AsyncConfigurer`:`AsyncConfigurere` 是一个 Spring 提供的接口,包含两个方法。一个可以重载 `TaskExecutor`(线程池),另一个是 exception handler。Exception handler 支持注入用来捕捉 unCaught 异常,也可以自己定义 class 直接实现。这里我不会直接实现,而是用 Spring 提供的 `AsyncConfigurerSupport` 类,通过 `@Configuration` 和 `@EnableAsync` 注解提供默认实现。
```java package com.example.ask2shamik.springAsync; import java.lang.reflect.Method; import java.util.concurrent.Executor; import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; import org.springframework.context.annotation.Configuration; import org.springframework.core.task.SimpleAsyncTaskExecutor; import org.springframework.lang.Nullable; import org.springframework.scheduling.annotation.AsyncConfigurerSupport; import org.springframework.scheduling.annotation.EnableAsync; @Configuration @EnableAsync public class CustomConfiguration extends AsyncConfigurerSupport { @Override public Executor getAsyncExecutor() { return new SimpleAsyncTaskExecutor(); } @Override @Nullable public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { return (throwable, method, obj) -> { System.out.println("Exception Caught in Thread - " + Thread.currentThread().getName()); System.out.println("Exception message - " + throwable.getMessage()); System.out.println("Method name - " + method.getName()); for (Object param : obj) { System.out.println("Parameter value - " + param); } }; } } ```
请注意,因为不想使用自定义 task executor,在 `getAsyncExecutor` 方法中,没有创建任何新的 executor。因此,我将使用 Spring 默认的 `SimpleAsyncExecutor`。
但是,我需要自己定义 uncaught exception handler 处理 uncaught 异常。因此,我写了一条继承 `AsyncUncaughtExceptionHandler` 类的 lambda 表达式并覆盖 `handleuncaughtexception` 方法。
这样,会让 Spring 加载与应用匹配的 `AsyncConfugurer(CustomConfiguration)` 并用 lambda 表达式进行异常处理。
新建一个 `@Async` 方法抛出异常:
```java package com.example.ask2shamik.springAsync; import java.util.Map; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Component; @Component public class AsyncMailTrigger { @Async public void senMailwithException() throws Exception { throw new Exception("SMTP Server not found :: orginated from Thread :: " + Thread.currentThread().getName()); } } ```
现在,创建调用方法。
```java package com.example.ask2shamik.springAsync; import java.util.HashMap; import java.util.Map; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Component; @Component public class AsyncCaller { @Autowired AsyncMailTrigger asyncMailTriggerObject; public void rightWayToCall() throws Exception { System.out.println("Calling From rightWayToCall Thread " + Thread.currentThread().getName()); asyncMailTriggerObject.senMailwithException(); } } ```
接下来让我们启动 Spring Boot 应用,看它如何捕捉 `sendMailwithException` 方法引发的异常。
```java package com.example.ask2shamik.springAsync; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.scheduling.annotation.EnableAsync; import com.example.ask2shamik.springAsync.demo.AsyncCaller; @SpringBootApplication public class DemoApplication { @Autowired AsyncCaller caller; public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } @Bean public CommandLineRunner commandLineRunner(ApplicationContext ctx) { return args -> { caller.rightWayToCall(); }; } } ```
结果如下:
```shell Calling From rightWayToCall Thread main Exception Caught in Thread - SimpleAsyncTaskExecutor-1 Exception message - SMTP Server not found:: originated from Thread:: SimpleAsyncTaskExecutor-1 Method name - senMailwithException ```
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://yundeesoft.com/13457.html