首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >观察者模式-Spring事件机制的应用

观察者模式-Spring事件机制的应用

作者头像
java技术爱好者
发布2020-09-22 16:20:01
7900
发布2020-09-22 16:20:01
举报
文章被收录于专栏:java技术爱好者java技术爱好者

定义

观察者模式(Observer),又叫发布-订阅模式(Publish/Subscribe),定义对象间一种一对多的依赖关系,使得每当一个对象改变状态,则所有依赖于它的对象都会得到通知并自动更新。

通俗解释

比如我们在宿舍打斗地主,我们要找个人来“放风”,这个人在门口观察,如果有宿管过了检查,那么就通知宿舍其他的小伙伴停止斗地主回床上睡觉。这种模式就被称为观察者模式。 从这个例子看,“宿管是否过来宿舍”是订阅的主题,观察者是放风的人,订阅者是打斗地主的小伙伴,被观察者就是宿管。

不使用观察者模式的问题

假设我们基于之前在策略模式讲的电子支付的例子,支付完成后要发送消息,发送的消息有:短信,公众号消息,APP站内消息,邮箱。如果不使用观察者模式,怎么做呢?看代码:

    @Override
    public String pay(String channel, String amount) throws Exception {
        PayStrategy payStrategy = PayStrategyFactory.getPayStrategy(channel);
        if(payStrategy == null){
            return "输入渠道码有误";
        }
        String msg = payStrategy.pay(channel, amount);
        //发送短信
        System.out.println("发送短信:"+msg);
        //发送微信公众号消息
        System.out.println("发送微信公众号消息:"+msg);
        //发送邮件
        System.out.println("发送邮件:"+msg);
        //发送APP系统信息
        System.out.println("发送APP系统信息:"+msg);
        return msg;
    }

启动项目是没有问题的,我们调用接口后可以看到控制台打印以下信息:

/**
发送短信:使用 支付宝支付 ,消费了 100 元
发送微信公众号消息:使用 支付宝支付 ,消费了 100 元
发送邮件:使用 支付宝支付 ,消费了 100 元
发送APP系统信息:使用 支付宝支付 ,消费了 100 元
*/

但是我们很明显可以看出有以下的问题:

  • 每次支付如果需要新增一种消息通知方式,则要修改原来的类,不利于维护。
  • 违反了开闭原则,对拓展开放,对修改关闭。
  • 违反了单一职责原则,支付不应该糅杂消息通知的功能。

上面就从代码演示了为什么要使用观察者模式,很多文章说不清楚,单纯地抛出一个概念和一些简单的例子,实际项目中肯定是没有那么简单。

使用观察者模式优化

这里的话,我不使用java自带的ObserverObservable来做,因为实际项目中一般都会使用Spring框架,Spring框架有一个事件机制,也是使用观察者模式的这种设计模式,而且在实际项目中我们往往会采用这种成熟度更高的框架,就像代理模式我们也很少会直接使用原生的JDK动态代理,而是采用SpringAOP来实现。

创建支付的事件

//继承ApplicationEvent类
public class PayEvent extends ApplicationEvent {
    //消息体
    private Map<String,String> map;
    //订阅主题
    private String topic;

    public PayEvent(Object source, Map<String, String> map, String topic) {
        //调用父类的构造器
        super(source);
        this.map = map;
        this.topic = topic;
    }

    public Map<String, String> getMap() {
        return map;
    }
    public void setMap(Map<String, String> map) {
        this.map = map;
    }
    public String getTopic() {
        return topic;
    }
    public void setTopic(String topic) {
        this.topic = topic;
    }
}

创建事件监听类

//短信监听,实现ApplicationListener接口,重写onApplicationEvent()方法
@Component
public class SmsListener implements ApplicationListener<PayEvent> {

    @Override
    public void onApplicationEvent(PayEvent payEvent) {
        //订阅主题
        String topic = payEvent.getTopic();
        //消息体
        Map<String, String> map = payEvent.getMap();
        //发送短信
        System.out.println("订阅主题是:" + topic + ";发送短信:" + map.get("msg"));
    }
}
//公众号监听
@Component
public class WechatListener implements ApplicationListener<PayEvent> {

    @Override
    public void onApplicationEvent(PayEvent payEvent) {
        String topic = payEvent.getTopic();
        Map<String, String> map = payEvent.getMap();
        System.out.println("订阅主题是:" + topic + ";发送公众号消息:" + map.get("msg"));
    }
}
//邮箱监听
@Component
public class MailListener implements ApplicationListener<PayEvent> {

    @Override
    public void onApplicationEvent(PayEvent payEvent) {
        String topic = payEvent.getTopic();
        Map<String, String> map = payEvent.getMap();
        System.out.println("订阅主题是:" + topic + ";发送邮件:" + map.get("msg"));
    }
}
//App站内消息监听
@Component
public class AppListener implements ApplicationListener<PayEvent> {

    @Override
    public void onApplicationEvent(PayEvent payEvent) {
        String topic = payEvent.getTopic();
        Map<String, String> map = payEvent.getMap();
        System.out.println("订阅主题是:" + topic + ";发送App站内消息:" + map.get("msg"));
    }
}

重构PayServiceImpl类

    @Override
    public String pay(String channel, String amount) throws Exception {
        PayStrategy payStrategy = PayStrategyFactory.getPayStrategy(channel);
        if(payStrategy == null){
            return "输入渠道码有误";
        }
        String msg = payStrategy.pay(channel, amount);
        Map<String,String> map = new HashMap<>();
        map.put("msg",msg);
        //创建一个支付事件
        PayEvent payEvent = new PayEvent(this, map, "支付");
        //获取Spring的ApplicationContext容器,发布事件,监听类监听到事件后就会发送消息
        SpringContextUtil.getApplicationContext().publishEvent(payEvent);
        return msg;
    }

然后我们启动项目,调用接口,控制台就可以打印的信息:

/**
订阅主题是:支付;发送App站内消息:使用 支付宝支付 ,消费了 100 元
订阅主题是:支付;发送邮件:使用 支付宝支付 ,消费了 100 元
订阅主题是:支付;发送短信:使用 支付宝支付 ,消费了 100 元
订阅主题是:支付;发送公众号消息:使用 支付宝支付 ,消费了 100 元
*/

异步监听事件,实现解耦

改造之后是否就一劳永逸了呢,实际上并非如此。因为上面的消息发送的监听类是同步的,也就是如果发送消息出现异常,那就会导致支付的接口无法正常返回。请看以下代码:

@Component
public class WechatListener implements ApplicationListener<PayEvent> {

    @Override
    public void onApplicationEvent(PayEvent payEvent) {
        String topic = payEvent.getTopic();
        Map<String, String> map = payEvent.getMap();
        //在发送微信公众号消息的逻辑中制造异常
        System.out.println(10 / 0);
        System.out.println("订阅主题是:" + topic + ";发送公众号消息:" + map.get("msg"));
    }
}
    @Override
    public String pay(String channel, String amount) throws Exception {
        PayStrategy payStrategy = PayStrategyFactory.getPayStrategy(channel);
        if(payStrategy == null){
            return "输入渠道码有误";
        }
        String msg = payStrategy.pay(channel, amount);
        Map<String,String> map = new HashMap<>();
        map.put("msg",msg);
        //创建一个支付事件
        PayEvent payEvent = new PayEvent(this, map, "支付");
        //获取Spring的ApplicationContext容器,发布事件
        SpringContextUtil.getApplicationContext().publishEvent(payEvent);
        //发送消息后的逻辑,打印日志到控制台
        System.out.println("发送消息后的逻辑代码...");
        return msg;
    }

我们在发送公众号消息的逻辑里制造了一个异常,然后在pay()方法中加了一个打印日志在发布支付的事件后面,接下来调用接口,结果是:

/**
订阅主题是:支付;发送App站内消息:使用 支付宝支付 ,消费了 100 元
订阅主题是:支付;发送邮件:使用 支付宝支付 ,消费了 100 元
订阅主题是:支付;发送短信:使用 支付宝支付 ,消费了 100 元
java.lang.ArithmeticException: / by zero
......
*/

发送消息后的逻辑是没有被执行。这样显然是不符合业务要求的,因为在很多时候,发送消息失败是不能影响支付流程的,应该异步进行。怎么异步进行发送消息呢?

很简单,只需要两个步骤。

第一步:在监听类或者方法上添加@Async注解,例如:

@Component
@Async//加上异步执行的注解
public class WechatListener implements ApplicationListener<PayEvent> {

    @Override
    public void onApplicationEvent(PayEvent payEvent) {
        String topic = payEvent.getTopic();
        Map<String, String> map = payEvent.getMap();
        System.out.println(10 / 0);
        System.out.println("订阅主题是:" + topic + ";发送公众号消息:" + map.get("msg"));
    }
}

第二步:在SpringBoot启动类上添加@EnableAsync注解,例如:

@SpringBootApplication
@EnableAsync//添加启用异步的注解
public class StrategyApplication {
    public static void main(String[] args) {
        SpringApplication.run(StrategyApplication.class, args);
    }
}

然后就可以实现异步监听了,调用接口,我们可以看到控制台打印的日志如下:

/**
订阅主题是:支付;发送App站内消息:使用 支付宝支付 ,消费了 100 元
订阅主题是:支付;发送邮件:使用 支付宝支付 ,消费了 100 元
订阅主题是:支付;发送短信:使用 支付宝支付 ,消费了 100 元
发送消息后的逻辑代码...
使用 支付宝支付 ,消费了 100 元
java.lang.ArithmeticException: / by zero
......
*/

明显可以看到支付后的逻辑也能正常执行下去,证明实现了异步监听!

扩展

Spring里提供了许多的监听器,这里只是介绍了其中一种。

还有一种叫SpringApplicationRunListener也是很常用的监听器,可以监听SpringBoot项目启动的事件,用于在启动项目时加载一些配置。

还有一种叫SmartApplicationListener,这种监听器可以设置优先级。假设发送消息需要按顺序先发送短信,再发送公众号,再发送邮箱…,那就可以使用这种监听器实现,这里就不多做介绍了,小伙伴有兴趣的话,我可以再写一篇文章详细介绍。

总结

经过重构之后,我们可以明显看到,如果以后要增加一种新的消息通知方式,是不需要修改PayServiceImpl的,我们只需要再增加一个监听类即可,这就符合了开闭原则。有利于代码的维护。而且最重要是解耦,支付的业务逻辑和发送消息的业务逻辑不会再糅合在一起了,符合职责单一原则。

在很多框架中,观察者模式都有应用,对于学习很多例如zookeeper消息中间件微服务注册中心等知识是有很大帮助的。在实际项目中,观察者模式也是一种很常用的设计模式。比如有一种业务场景,通讯录的部门里有员工离职,需要通知其他依赖于通讯录的应用都要同步部门的员工,那就可以使用这种方式来实现。

能力有限,如果有什么错误或者不当之处,请大家批评指正,一起学习交流!

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-05-04,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 java技术爱好者 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 定义
  • 通俗解释
  • 不使用观察者模式的问题
  • 使用观察者模式优化
  • 创建支付的事件
  • 创建事件监听类
  • 重构PayServiceImpl类
  • 异步监听事件,实现解耦
  • 扩展
  • 总结
相关产品与服务
短信
腾讯云短信(Short Message Service,SMS)可为广大企业级用户提供稳定可靠,安全合规的短信触达服务。用户可快速接入,调用 API / SDK 或者通过控制台即可发送,支持发送验证码、通知类短信和营销短信。国内验证短信秒级触达,99%到达率;国际/港澳台短信覆盖全球200+国家/地区,全球多服务站点,稳定可靠。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档