浅谈spring事件业务解耦与异步调用
使用spring的事件机制有助于对我们的项目进一步的解耦。假如现在我们面临一个需求:
我需要在用户注册成功的时候,根据用户提交的邮箱、手机号信息,向用户发送邮箱认证和手机号短信通知。传统的做法之一是在我们的UserService层注入邮件发送和短信发送的相关类,然后在完成用户注册同时,调用对应类方法完成邮件发送和短信发送
但这样做的话,会把我们邮件、短信发送的业务与我们的UserService的逻辑业务耦合在了一起。耦合造成的常见缺点是,我(甚至假设很频繁的)修改了邮件、短信发送的API,我就可能需要在UserService层修改相应的调用方法,但这样做人家UserService就会很无辜并吐槽: 你改邮件、短信发送的业务,又不关我的事,干嘛老改到我身上来了?这就是你的不对了。
对呀!根据职责分明的设计原则,人家UserService就只该管用户管理部分的业务逻辑,你老让它干别人干的事,它当然不高兴了!
那该怎么拌?凉拌?不不不。。。我们可以通过spring的事件机制来实现解耦呀。利用观察者设计模式,设置监听器来监听userService的注册事件(同时,我们可以很自然地将userService理解成了 事件发布者),一旦userService注册了,监听器就完成相应的邮箱、短信发送工作(同时,我们也可以很自然地将 发送邮件、 发送短信理解成我们的 事件源)。这样userService就不用管别人的事了,只需要在完成注册功能时候,当下老大,号令手下(监听器),让它完成短信、邮箱的发送工作。
spring的事件通信常按下列流程进行
Created with Raphaël 2.1.0事件发布者广播事件(源)监听器收到广播,获取事件源监听器根据事件源采取相应的处理措施
事件实例分析
在这里面,我们涉及到三个主要对象:事件发布者、事件源、事件监听器。根据这三个对象,我们来配置我们的注册事件实例:
1. 定义事件源
利用事件通信的第一步往往便是定义我们的事件。在spring中,所有事件都必须扩展抽象类ApplicationEvent,同时将事件源作为构造函数参数,在这里,我们定义了发邮件、发短信两个事件如下所示
/*****************邮件发送事件源*************/publicclassSendEmailEventextendsApplicationEvent{//定义事件的核心成员:发送目的地,共监听器调用完成邮箱发送功能privateString emailAddress; publicSendEmailEvent(Object source,String emailAddress ) { //source字面意思是根源,意指发送事件的根源,即我们的事件发布者super(source); this.emailAddress = emailAddress; } publicString getEmailAddress() { returnemailAddress; } } /*****************短信发送事件源*************/publicclasssendMessageEventextendsApplicationEvent{privateString phoneNum; publicsendMessageEvent(Object source,String phoneNum ) { super(source); this.phoneNum = phoneNum; } publicString getPhoneNum() { returnphoneNum; } }
2. 定义事件监听器
事件监听类需要实现我们的ApplicationListener接口,除了可以实现ApplicationListener定义事件监听器外,我们还可以让事件监听类实现SmartApplicationListener(智能监听器)接口,。关于它的具体用法和实现可参考我的下一篇文章《spring学习笔记(14)趣谈spring 事件机制[2]:多监听器流水线式顺序处理 》。而此外,如果我们事件监听器监听的事件类型唯一的话,我们可以通过泛型来简化配置。
现在我们先来看看本例定义:
publicclassRegisterListenerimplementsApplicationListener{/* *当我们的发布者发布时间时,我们的监听器收到信号,就会调用这个方法 *我们对其进行重写来适应我们的需求 *@Param event:我们的事件源 */@OverridepublicvoidonApplicationEvent(ApplicationEvent event) { //我们定义了两个事件:发短信,发邮箱,他们一旦被发布都会被此方法调用//于是我们需要判断当前event的具体类型if(event instanceofSendEmailEvent){//如果是发邮箱事件System.out.println(“正在向”+ ((SendEmailEvent) event).getEmailAddress()+ “发送邮件。。.。。.”);//interwetten与威廉的赔率体系 发送邮件事件try{ Thread.sleep(1* 1000);//模拟请求邮箱服务器、验证账号密码,发送邮件耗时。} catch(InterruptedException e) { e.printStackTrace(); } System.out.println(“邮件发送成功!”); }elseif(event instanceofsendMessageEvent){//是发短信事件event = (sendMessageEvent) event; System.out.println(“正在向”+ ((sendMessageEvent) event).getPhoneNum()+ “发送短信。。.。。.”);//模拟发送邮短信事件try{ Thread.sleep(1* 1000);//模拟发送短信过程} catch(InterruptedException e) { e.printStackTrace(); } System.out.println(“短信发送成功!”); } } } /******************通过泛型配置实例如下******************/publicclassRegisterListenerimplementsApplicationListener《SendEmailEvent》 {//这里使用泛型@Override//因为使用了泛型,我们的重写方法入参事件就唯一了。publicvoidonApplicationEvent(SendEmailEvent event) { 。。.。。 } 。。.。 }
3. 定义事件发布者
事件发送的代表类是ApplicationEventPublisher我们的事件发布类常实现ApplicationEventPublisherAware接口,同时需要定义成员属性ApplicationEventPublisher来发布我们的事件。
除了通过实现ApplicationEventPublisherAware外,我们还可以实现ApplicationContextAware接口来完成定义,ApplicationContext接口继承了ApplicationEventPublisher。ApplicationContext是我们的事件容器上层,我们发布事件,也可以通过此容器完成发布。下面使用两种方法来定义我们的发布者
在本例中,我们的时间发布者自然就是我们的吐槽者,userService:
/**********方法一:实现除了通过实现ApplicationEventPublisherAware接口************/publicclassUserServiceimplementsApplicationEventPublisherAware{privateApplicationEventPublisher applicationEventPublisher;//底层事件发布者@OverridepublicvoidsetApplicationEventPublisher(//通过Set方法完成我们的实际发布者注入 ApplicationEventPublisher applicationEventPublisher) { this.applicationEventPublisher = applicationEventPublisher; } publicvoiddoLogin(String emailAddress,String phoneNum) throwsInterruptedException{ Thread.sleep(200);//模拟用户注册的相关业务逻辑处理System.out.println(“注册成功!”); //下列向用户发送邮件SendEmailEvent sendEmailEvent = newSendEmailEvent(this,emailAddress);//定义事件sendMessageEvent sendMessageEvent = newsendMessageEvent(this, phoneNum); applicationEventPublisher.publishEvent(sendEmailEvent);//发布事件applicationEventPublisher.publishEvent(sendMessageEvent); } //。。.忽略其他用户管理业务方法} /**********方法二:实现除了通过实现ApplicationContext接口************/publicclassUserService2implementsApplicationContextAware{privateApplicationContext applicationContext; @OverridepublicvoidsetApplicationContext(ApplicationContext applicationContext) throwsBeansException { this.applicationContext = applicationContext; } publicvoiddoLogin(String emailAddress,String phoneNum) throwsInterruptedException{ Thread.sleep(200);//模拟用户注册的相关业务逻辑处理System.out.println(“注册成功!”); //下列向用户发送邮件SendEmailEvent sendEmailEvent = newSendEmailEvent(this,emailAddress);//定义事件sendMessageEvent sendMessageEvent = newsendMessageEvent(this, phoneNum); applicationContext.publishEvent(sendEmailEvent);//发布事件applicationContext.publishEvent(sendMessageEvent); } //。。.忽略其他用户管理业务方法}
4. 在IOC容器注册监听器
《!-- 在spring容器中注册事件监听器, 应用上下文将会识别实现了ApplicationListener接口的Bean, 并在特定时刻将所有的事件通知它们 --》《beanid=“RegisterListener”class=“test.event.RegisterListener”/》《!-- 注册我们的发布者,后面测试用到 --》《beanid=“userService”class=“test.event.UserService”/》
5. 测试方法
publicstaticvoidmain(String args[]) throwsInterruptedException{ ApplicationContext ac = newClassPathXmlApplicationContext(“classpath:test/event/event.xml”); UserService userService = (UserService) ac.getBean(“userService”); Long beginTime = System.currentTimeMillis(); userService.doLogin(“zenghao@google.com”,“12345678911”);//完成注册请求System.out.println(“处理注册相关业务耗时”+ (System.currentTimeMillis() - beginTime )+ “ms”); System.out.println(“处理其他业务逻辑”); Thread.sleep(500);//模拟处理其他业务请求耗时System.out.println(“处理所有业务耗时”+ (System.currentTimeMillis() - beginTime )+ “ms”); System.out.println(“向客户端发送注册成功响应”); }
6. 测试结果及分析
调用上面测试方法,控制台打印信息
注册成功!
正在向zenghao@google.com发送邮件……
邮件发送成功!
正在向12345678911发送短信……
发送成功!
处理注册相关业务耗时2201ms
处理其他业务逻辑开始。。
处理其他业务逻辑结束。。
处理所有业务耗时2701ms
向客户端发送注册成功响应
在本例中,我们通过事件机制完成了userService和邮件、短信发送业务的解耦。但观察我们的测试结果,我们会发现,这样的用户体验真是糟糕透了:天呐,我去你那注册个用户,要我等近3秒钟!这太久了!
为什么会这么久?我们根据方法分析:
1. 注册查询数据库用了200ms(查询用户名、邮箱、手机号有没被使用,插入用户信息到数据库等操作)
2. 发送邮件用了1000ms
3. 发送短信用了1000ms
4. 处理其他业务逻辑(保存用户信息到session,其他信息数据处理等)
第1,4步的时间耗损我们很难优化,但2,3步是主要耗时的地方,我们能不能想办法把它缩减掉了,它把我们的正常的业务处理堵塞了。什么?堵塞,想到堵塞,我们会很自然地想到非堵塞,那就通过异步来完成2,3呗!
7. 异步拓展。
在spring3以上,拓展了自己独立的时间机制,我们可以使用@Async来完成异步配置。
首先我们需要在我们的IOC容器增加
《!--先在命名空间中增加我们的task标签,注意它们的添加位置 xmlns 多加下面的内容: xmlns:task=“http://www.springframework.org/schema/task” 然后xsi:schemaLocation多加下面的内容 http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-3.0.xsd --》《!-- 我们的异步事件配置,非常简单 --》《!--开启注解调度支持 @Async @Scheduled--》《task:annotation-driven/》
然后在我们的事件监听器中添加@Async注解
/***************我们可以在类名上添加****************/@AsyncpublicclassRegisterListenerimplementsApplicationListener{。。.。。. } /****************也可以在方法体上添加************/@AsyncpublicclassRegisterListenerimplementsApplicationListener{@OverridepublicvoidonApplicationEvent(ApplicationEvent event) { 。。.。。 } }
然后,再调用我们的同样的测试方法,这次我们的结果变成:
注册成功!
正在向zenghao@google.com发送邮件……
处理注册相关业务耗时201ms ————此时邮件发送还没有结束,和邮件发送异步了
正在向12345678911发送短信…。。 ————–短信发送和邮件发送和主业务处理程序都异步了!
处理其他业务逻辑开始。。
处理其他业务逻辑结束。。
处理所有业务耗时701ms
向客户端发送注册成功响应 ——客户端耗时701ms就收到响应了。
邮件发送成功! —-这个时候邮箱才发完
短信发送成功!
从以上的测试结果我们,我们的邮箱发送和短信发送都 分别单独地异步完成了,大大缩短了我们主业务处理事件,也提高了用户体验
小结
从本例可以看出,不同业务功能的生硬组合,会出现逻辑处理混乱的严重耦合现象,比如userService类既处理自己的用户逻辑,还要处理邮箱等发送的逻辑,这是不是也意味着,如果以后我们拓展更多的功能,我们的userService类还要出现更多的逻辑处理,来个大杂烩?,这同时还可能会为我们主要业务处理带来不必要的阻塞。当然,为了防止阻塞,我们还可以创建新的线程来异步,但这样原来的类就显得更加杂乱臃肿了。使用spring事件机制能很好地帮助我们消除不同业务间的深耦合关系。它强大的任务调度还能帮助我们简洁地实现事件异步。关于事件的一些其他用法可参考我的下一篇博文《趣谈spring 事件机制[2]:多监听器流水线式顺序处理》 关于任务调度的相关框架和使用可参考我的专栏《深入浅出Quartz任务调度》。
非常好我支持^.^
(0) 0%
不好我反对
(0) 0%