引言有点长
前端的宝宝会用ajax,用异步编程到快乐的不行~ 我们java也有异步,用起来比他们还快乐~ 我们bia~ji~一个注(gǒupí)解(gāoyào)
,也是快乐风男... 且看下面的栗子:
注册一个用户,给他的账户初始化积分(也可以想象成注册奖励),再给用户发个注册通知短信,再发个邮件,(只是举栗子,切莫牛角大法
),这样一个流程,比如我们加上了事务(一损俱损的那种
),短信和邮件我觉得完全可以拆分出来,没必要托在主流程上来:
一号坑 循环依赖
看code:
@Service
public class UserServiceImpl implements UserService {
@Autowired
UserService userService;
@Override
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
public int save(UserDTO userDTO) {
User user = new User();
BeanCopyUtils.copy(userDTO, user);
int insert = userMapper.insert(user);
System.out.println("User 保存用户成功:" + user);
userService.senMsg(user);
userService.senEmail(user);
return insert;
}
@Async
@Override
public Boolean senMsg(User user) {
try {
TimeUnit.SECONDS.sleep(2);
System.out.println("发送短信中:.....");
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "给用户id:" + user.getId() + ",手机号:" + user.getMobile() + "发送短信成功");
return true;
}
@Async
@Override
public Boolean senEmail(User user) {
try {
TimeUnit.SECONDS.sleep(3);
System.out.println("发送邮件中:.....");
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "给用户id:" + user.getId() + ",邮箱:" + user.getEmail() + "发送邮件成功");
return true;
}
复制代码
结果:启动不起来,Spring循环依赖问题。 或许刚说到这,有的小伙伴就会大惊失色了。Spring不是解决了循环依赖问题吗,它是支持循环依赖的呀?怎么会呢?
不可否认,在这之前我也是这么坚信的,而且每次使用得也屡试不爽。倘若你目前也和我有一样坚挺的想法,那就让异常UnsatisfiedDependencyException
,has been injected into other beans [userServiceImpl] in its raw version as part of a circular reference,
,来鼓励你,拥抱你, 就是这么的不给面子,赤裸裸的circular reference
。
谈到Spring Bean的循环依赖,有的小伙伴可能比较陌生,毕竟开发过程中好像对循环依赖这个概念无感知。其实不然,你有这种错觉,权是因为你工作在Spring的襁褓中,从而让你“高枕无忧”~ 其实我们的代码中可定被我们写了循环依赖,比如像这样:
@Service
public class AServiceImpl implements AService {
@Autowired
private BService bService;
...
}
@Service
public class BServiceImpl implements BService {
@Autowired
private AService aService;
...
}
复制代码
通过实验总结出,出现使用@Async导致循环依赖问题的必要条件:
- 已开启@EnableAsync的支持
- @Async注解所在的Bean被循环依赖了
二号坑 异步失效
那么既然不能循环依赖,我们就不循环依赖,我们这么来:
@Service
public class UserServiceImpl implements UserService {
@Autowired
UserMapper userMapper;
@Autowired
SendService sendService;
@Override
@Transactional()
public int save(UserDTO userDTO) {
User user = new User();
BeanCopyUtils.copy(userDTO, user);
int insert = userMapper.insert(user);
System.out.println("User 保存用户成功:" + user);
this.senMsg(user);
this.senEmail(user);
return insert;
}
@Async
@Override
public Boolean senMsg(User user) {
System.out.println(Thread.currentThread().getName() + "给用户id:" + user.getId() + ",手机号:" + user.getMobile() + "发送短信成功");
return true;
}
@Async
@Override
public Boolean senEmail(User user) {
System.out.println(Thread.currentThread().getName() + "给用户id:" + user.getId() + ",邮箱:" + user.getEmail() + "发送邮件成功");
return true;
}
复制代码
结果我们测试了几把,我打印一下结果:
2019-08-05 21:59:32.304 INFO 14360 --- [nio-8080-exec-3] com.alibaba.druid.pool.DruidDataSource : {dataSource-1} inited
2019-08-05 21:59:32.346 DEBUG 14360 --- [nio-8080-exec-3] c.b.lea.mybot.mapper.UserMapper.insert : ==> Preparing: insert into t_user (username, sex, mobile,email) values (?, ?, ?,?)
2019-08-05 21:59:32.454 DEBUG 14360 --- [nio-8080-exec-3] c.b.lea.mybot.mapper.UserMapper.insert : ==> Parameters: 王麻子(String), 男(String), 18820158833(String), [email protected](String)
2019-08-05 21:59:32.463 DEBUG 14360 --- [nio-8080-exec-3] c.b.lea.mybot.mapper.UserMapper.insert : <== Updates: 1
User 保存用户成功:User(id=101, username=王麻子, mobile=18820158833, [email protected], sex=男, password=123435, createTime=Mon Aug 05 12:20:51 CST 2019, updateTime=null)
发送短信中:.....
http-nio-8080-exec-3给用户id:101,手机号:18820158833发送短信成功
发送邮件中:.....
http-nio-8080-exec-3给用户id:101,邮箱:[email protected]发送邮件成功
复制代码
这不白瞎了吗?感知不到我的爱,白写了,难受~~线程依然是http-nio-8080-exec-3
,那么为什么了呢? 下面会讲的哦,先说结论:
通过实验总结出,出现使用@Async导致异步失效的原因:
- 在本类中使用了异步是不支持异步的
- 调用者其实是this,是当前对象,不是真正的代理对象
userService
,spring无法截获这个方法调用 所以不在不在本类中去调用,网上的解决方法有applicationContext.getBean(AInterface.class)
和AopContext.currentProxy()
,但是我目前没试成功,依旧在报错阶段,等我研究成功补充到这里先占位...
三号坑 基本类型
我想写个看短信啊,邮件发送成功的标志,来走起,
四号坑 事务失效
万一这短信接口(大多数我们都使用三方接口),发邮件有点不舒服(出异常了...)
事故处理
你需要懂的异步原理
@Async的异步:
- 实际是spring 在扫描bean的时候会扫描方法上是否包含@Async的注解,如果包含的,spring会为这个bean动态的生成一个子类,我们称之为代理类(jdkProxy), 代理类是继承我们所写的bean的,然后把代理类注入进来,那此时,在执行此方法的时候,会到代理类中,代理类判断了此方法需要异步执行,就不会调用父类 (我们原本写的bean)的对应方法。
- spring自己维护了一个队列,他会把需要执行的方法,放入队列中,等待线程池去读取这个队列,完成方法的执行, 从而完成了异步的功能。
- 我们可以关注到再配置task的时候,是有参数让我们配置线程池的数量的。因为这种实现方法,所以在同一个类中的方法调用,添加@Async注解是失效的!,原因是当你在同一个类中的时候,方法调用是在类体内执行的,spring无法截获这个方法调用(为什么呢,这个就是下文讲的...
奸笑...嘻嘻嘻嘻...
)。 - 那在深入一步,Spring为我们提供了AOP,面向切面的功能。他的原理和异步注解的原理是类似的,spring在启动容器的时候,会扫描切面所定义的 类。在这些类被注入的时候,所注入的也是代理类,当你调用这些方法的时候,本质上是调用的代理类。通过代理类再去执行父类相对应的方法,那spring只需要在调用之前和之后执行某段代码就完成了AOP的实现了!
SpringBoot环境中,要使用@Async注解,我们需要先在启动类上加上@EnableAsync注解。这个与在SpringBoot中使用@Scheduled注解需要在启动类中加上@EnableScheduling是一样的道理(当然你使用古老的XML配置也是可以的,但是在SpringBoot环境中,建议的是全注解开发),具体原理下面会分析。加上@EnableAsync注解后,如果我们想在调用一个方法的时候开启一个新的线程开始异步操作,我们只需要在这个方法上加上@Async注解,当然前提是,这个方法所在的类必须在Spring环境中。
示例:非spingboot项目
<task:annotation-driven executor="annotationExecutor" />
<!-- 支持 @Async 注解 -->
<task:executor id="annotationExecutor" pool-size="20"/>
复制代码
执行流程:
- 扫描是否开启注解
EnableAsync
,@EnableAsync注解上有个@Import(AsyncConfigurationSelector.class)
,springboot的注入老套路了 - 请您再移步
AsyncConfigurationSelector
,看到selectImports
方法了没,这里使用的是默认使用的是ProxyAsyncConfiguration
这个配置类 - 继续观摩
ProxyAsyncConfiguration
继承AbstractAsyncConfiguration
,它里面的的setConfigurers
说明了我们可以通过实现AsyncConfigurer
接口来完成线程池以及异常处理器的配置,而且在Spring环境中只能配置一个实现类,否则会抛出异常。 上一点代码:
/**
* Collect any {@link AsyncConfigurer} beans through autowiring.
*/
@Autowired(required = false)
void setConfigurers(Collection<AsyncConfigurer> configurers) {
if (CollectionUtils.isEmpty(configurers)) {
return;
}
//AsyncConfigurer用来配置线程池配置以及异常处理器,而且在Spring环境中最多只能有一个,在这里我们知道了,如果想要自己去配置线程池,只需要实现AsyncConfigurer接口,并且不可以在Spring环境中有多个实现AsyncConfigurer的类。
if (configurers.size() > 1) {
throw new IllegalStateException("Only one AsyncConfigurer may exist");
}
AsyncConfigurer configurer = configurers.iterator().next();
this.executor = configurer.getAsyncExecutor();
this.exceptionHandler = configurer.getAsyncUncaughtExceptionHandler();
}
复制代码
ProxyAsyncConfiguration
注入的beanAsyncAnnotationBeanPostProcessor
,这个BeanPostBeanPostProcessor
很显然会对带有能够引发异步操作的注解(比如@Async
)的Bean进行处理- 我们注意到
AsyncAnnotationBeanPostProcessor
有重写父类的setBeanFactory
,这个方法是不是有点熟悉呢,它是BeanFactoryAware
接口中的方法,AsyncAnnotationBeanPostProcessor
的父类实现了这个接口,在我们很久之前分析过的Bean的初始化中,是有提到过这个接口的,实现了Aware类型接口的Bean,会在初始化Bean的时候调用相应的初始化方法,具体可以查看AbstractAutowireCapableBeanFactory#initializeBean(final String beanName, final Object bean, RootBeanDefinition mbd)
方法 - 处理Bean的
postProcessAfterInitialization
方法在祖先类AbstractAdvisingBeanPostProcessor
中。从源码中可以看到。AsyncAnnotationBeanPostProcessor
是对Bean进行后置处理的BeanPostProcessor
- 最后代理到
JdkDynamicAopProxy
的invoke方法中,是用了责任链模式:List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
,将代理进行拦截来执行,通知链会包含setBeanFactory()方法生成的通知,执行链会用于创建ReflectiveMethodInvocation
对象,最终是调用ReflectiveMethodInvocation
的proceed()
来完成对方法的增强处理,proceed()
方法在这里会执行最后一个分支 - 具体执行的是
AsyncExecutionInterceptor
的invoke()
- 注意:虽然上文Spring环境中只能有一个
AsyncConfigurer
实现类,但是不意味着,在Spring环境中只能配置一个线程池,在Spring环境中是可以配置多个线程池,而且我们可以在使用@Async注解进行异步操作的时候,通过在value属性上指定线程池BeanName,这样就可以指定相应的线程池来作为任务的载体,参见:determineAsyncExecutor
啰嗦兄弟:
当我们想要在SpringBoot中方便的使用@Async注解开启异步操作的时候,只需要实现AsyncConfigurer接口(这样就配置了默认线程池配置,当然该类需要在Spring环境中,因为是默认的,所以只能有一个,没有多个实现类排优先级的说法),实现对线程池的配置,并在启动类上加上@EnableAsync注解,即可使得@Async注解生效。
我们甚至可以不显式的实现AsyncConfigurer,我们可以在Spring环境中配置多个Executor类型的Bean,在使用@Async注解时,将注解的value指定为你Executor类型的BeanName,就可以使用指定的线程池来作为任务的载体,这样就使用线程池也更加灵活。
小甜点
所以:我们要用到亲爱的Spring的异步编程,异步编程有很多种方式:比如常见的Future的sync
,CompletableFuture.supplyAsync,
,@Async
,哈哈 其实都离不开Thread.start()...,等等我说个笑话:
老爸有俩孩子:小红和小明。老爸想喝酒了,他让小红去买酒,小红出去了。然后老爸突然想吸烟了,于是老爸让小明去买烟。在面对对象的思想中,一般会把买东西,然后买回来这件事作为一个方法,如果按照顺序结构或者使用多线程同步的话,小明想去买烟就必须等小红这个买东西的操作进行完。这样无疑增加了时间的开销(万一老爸尿憋呢?)。异步就是为了解决这样的问题。你可以分别给小红小明下达指令,让他们去买东西,然后你就可以自己做自己的事,等他们买回来的时候接收结果就可以了。
package com.boot.lea.mybot.futrue;
/**
* @ClassName: TestFuture
* @Description: 演示异步编程
* @author LiJing
* @date 2019/8/5 15:16
*/
@SuppressWarnings("all")
public class TestFuture {
static ExecutorService executor = Executors.newFixedThreadPool(2);
public static void main(String[] args) throws InterruptedException {
//两个线程的线程池
//小红买酒任务,这里的future2代表的是小红未来发生的操作,返回小红买东西这个操作的结果
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
System.out.println("爸:小红你去买瓶酒!");
try {
System.out.println("小红出去买酒了,女孩子跑的比较慢,估计5s后才会回来...");
Thread.sleep(5000);
return "我买回来了!";
} catch (InterruptedException e) {
System.err.println("小红路上遭遇了不测");
return "来世再见!";
}
}, executor);
//小明买烟任务,这里的future1代表的是小明未来买东西会发生的事,返回值是小明买东西的结果
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
System.out.println("爸:小明你去买包烟!");
try {
System.out.println("小明出去买烟了,可能要3s后回来...");
Thread.sleep(3000);
throw new InterruptedException();
// return "我买回来了!";
} catch (InterruptedException e) {
System.out.println("小明路上遭遇了不测!");
return "这是我托人带来的口信,我已经不在了。";
}
}, executor);
//获取小红买酒结果,从小红的操作中获取结果,把结果打印
future2.thenAccept((e) -> {
System.out.println("小红说:" + e);
});
//获取小明买烟的结果
future1.thenAccept((e) -> {
System.out.println("小明说:" + e);
});
System.out.println("爸:等啊等 西湖美景三月天嘞......");
System.out.println("爸: 我觉得无聊甚至去了趟厕所。");
Thread.currentThread().join(9 * 1000);
System.out.println("爸:终于给老子买来了......huo 酒");
//关闭线程池
executor.shutdown();
}
}
复制代码
运行结果: