hello个人博客

愿居于一城,与卿所见美好......

推荐文章

Java | Guava EventBus 使用 发布/订阅模式

    Java | Guava EventBus 使用 发布/订阅模式



    前言

    EventBus 是 Guava 的事件处理机制,是观察者模式(生产/消费模型)的一种实现。

    观察者模式在我们日常开发中使用非常广泛,例如在订单系统中,订单状态或者物流信息的变更会向用户发送APP推送、短信、通知卖家、买家等等;审批系统中,审批单的流程流转会通知发起审批用户、审批的领导等等。

    Observer模式也是 JDK 中自带就支持的,其在 1.0 版本就已经存在 Observer,不过随着 Java 版本的飞速升级,其使用方式一直没有变化,许多程序库提供了更加简单的实现,例如 Guava EventBus、RxJava、EventBus 等

    一、为什么要用 Observer模式以及 EventBus 优点 ?

    EventBus 优点

    • 相比 Observer 编程简单方便
    • 通过自定义参数可实现同步、异步操作以及异常处理
    • 单进程使用,无网络影响

    缺点

    • 只能单进程使用
    • 项目异常重启或者退出不保证消息持久化

    如果需要分布式使用还是需要使用 MQ

    二、EventBus 使用步骤

    1. 引入库

    Gradle

    compile group: 'com.google.guava', name: 'guava', version: '29.0-jre'
    

    Maven

    <dependency>
        <groupId>com.google.guava</groupId>
        <artifactId>guava</artifactId>
        <version>29.0-jre</version>
    </dependency>
    
    

    引入依赖后,这里我们主要使用 com.google.common.eventbus.EventBus 类进行操作,其提供了 registerunregisterpost 来进行注册订阅、取消订阅和发布消息

    public void register(Object object);
    
    public void unregister(Object object);
    
    public void post(Object event);
    

    2. 同步使用

    1. 首先创建一个 EventBus

    EventBus eventBus = new EventBus();
    

    2. 创建一个订阅者

    在 Guava EventBus 中,是根据参数类型进行订阅,每个订阅的方法只能由一个参数,同时需要使用 @Subscribe 标识

    class EventListener {
    
      /**
       * 监听 Integer 类型的消息
       */
      @Subscribe
      public void listenInteger(Integer param) {
        System.out.println("EventListener#listenInteger ->" + param);
      }
    
      /**
       * 监听 String 类型的消息
       */
      @Subscribe
      public void listenString(String param) {
        System.out.println("EventListener#listenString ->" + param);
      }
    }
    

    3. 注册到 EventBus 上并发布消息

    EventBus eventBus = new EventBus();
    
    eventBus.register(new EventListener());
    
    eventBus.post(1);
    eventBus.post(2);
    eventBus.post("3");
    

    运行结果为

    EventListener#listenInteger ->1
    EventListener#listenInteger ->2
    EventListener#listenString ->3
    

    根据需要我们可以创建多个订阅者完成订阅信息,同时如果一个类型存在多个订阅者,则所有订阅方法都会执行

    为什么说这么做是同步的呢?

    Guava Event 实际上是使用线程池来处理订阅消息的,通过源码可以看出,当我们使用默认的构造方法创建 EventBus 的时候,其中 executorMoreExecutors.directExecutor(),其具体实现中直接调用的 Runnable#run 方法,使其仍然在同一个线程中执行,所以默认操作仍然是同步的,这种处理方法也有适用的地方,这样既可以解耦也可以让方法在同一个线程中执行获取同线程中的便利,比如事务的处理

    EventBus 部分源码

    public class EventBus {
      private static final Logger logger = Logger.getLogger(EventBus.class.getName());
      private final String identifier;
      private final Executor executor;
      private final SubscriberExceptionHandler exceptionHandler;
      private final SubscriberRegistry subscribers;
      private final Dispatcher dispatcher;
    
      public EventBus() {
        this("default");
      }
    
      public EventBus(String identifier) {
        this(identifier, MoreExecutors.directExecutor(), Dispatcher.perThreadDispatchQueue(), EventBus.LoggingHandler.INSTANCE);
      }
    
      public EventBus(SubscriberExceptionHandler exceptionHandler) {
        this("default", MoreExecutors.directExecutor(), Dispatcher.perThreadDispatchQueue(), exceptionHandler);
      }
    
      EventBus(String identifier, Executor executor, Dispatcher dispatcher, SubscriberExceptionHandler exceptionHandler) {
        this.subscribers = new SubscriberRegistry(this);
        this.identifier = (String)Preconditions.checkNotNull(identifier);
        this.executor = (Executor)Preconditions.checkNotNull(executor);
        this.dispatcher = (Dispatcher)Preconditions.checkNotNull(dispatcher);
        this.exceptionHandler = (SubscriberExceptionHandler)Preconditions.checkNotNull(exceptionHandler);
      }
    }
    

    DirectExecutor 部分源码

    enum DirectExecutor implements Executor {
      INSTANCE;
    
      private DirectExecutor() {
      }
    
      public void execute(Runnable command) {
        command.run();
      }
    
      public String toString() {
        return "MoreExecutors.directExecutor()";
      }
    }
    

    3. 异步使用

    通过上面的源码,可以看出只要将构造方法中的 executor 换成一个线程池实现即可, 同时 Guava EventBus 为了简化操作,提供了一个简化的方案即 AsyncEventBus

    EventBus eventBus = new AsyncEventBus(Executors.newCachedThreadPool());
    
    • 1

    这样即可实现异步使用

    AsyncEventBus 源码

    public class AsyncEventBus extends EventBus {
      public AsyncEventBus(String identifier, Executor executor) {
        super(identifier, executor, Dispatcher.legacyAsync(), LoggingHandler.INSTANCE);
      }
    
      public AsyncEventBus(Executor executor, SubscriberExceptionHandler subscriberExceptionHandler) {
        super("default", executor, Dispatcher.legacyAsync(), subscriberExceptionHandler);
      }
    
      public AsyncEventBus(Executor executor) {
        super("default", executor, Dispatcher.legacyAsync(), LoggingHandler.INSTANCE);
      }
    }
    

    4. 异常处理

    如果处理时发生异常应该如何处理? 在看源码中,无论是 EventBus 还是 AsyncEventBus 都可传入自定义的 SubscriberExceptionHandler 该 handler 当出现异常时会被调用,我可可以从参数 exception 获取异常信息,从 context 中获取消息信息进行特定的处理

    其接口声明为

    public interface SubscriberExceptionHandler {
      /** Handles exceptions thrown by subscribers. */
      void handleException(Throwable exception, SubscriberExceptionContext context);
    }
    

    总结

    在上面的基础上,我们可以定义一些消息类型来实现不同消息的监听和处理,通过实现 SubscriberExceptionHandler 来处理异常的情况,无论时同步还是异步都能游刃有余

    参考

    • https://github.com/google/guava
    • https://github.com/greenrobot/EventBus
    • https://github.com/ReactiveX/RxJava

    阅读全文>>

作者:hello分类:【javaEE浏览(200评论(0

2020-12-22 18:15:59

关于REST风格的编程


    前言

    表现层状态转换(REST,英文:Representational State Transfer)是Roy Thomas Fielding博士于2000年在他的博士论文 中提出来的一种万维网软件架构风格,目的是便于不同软件/程序在网络(例如互联网)中互相传递信息。

    REST本身不是架构,只是一种架构风格,理解它的时候要参考这个架构风格出现的环境所施加的约束条件。
    REST的目的是“建立十年内不会过时的软件系统架构“,所以它具备三个特点:
    1. 状态无关 —— 确保系统的横向拓展能力
    2. 超文本驱动,Fielding的原话是”hypertext-driven" —— 确保系统的演化能力
    3. 对 resource 相关的模型建立统一的原语,例如:uri、http的method定义等 —— 确保系统能够接纳多样而又标准的客户端。

    这样对URL进行了限制,用于定义资源。简单来说就是定义了**如何设计系统来对外提供服务**。使用HTTP+JSON就可以搞定一切。

    REST详解

    REST究竟是什么?因为REST的内涵非常丰富,所以很难用一两句话解释清楚这个问题。

    首先,REST是Web自身的架构风格。REST也是Web之所以取得成功的技术架构方面因素的总结。REST是世界上最成功的分布式应用架构风格(成功案例:Web,还不够吗?)。它是为 运行在互联网环境 的 分布式 超媒体系统量身定制的。互联网环境与企业内网环境有非常大的差别,最主要的差别是两个方面:

    可伸缩性需求无法控制:并发访问量可能会暴涨,也可能会暴跌。

    安全性需求无法控制:无法控制客户端发来的请求的格式,很可能会是恶意的请求。

    而所谓的“超媒体系统”,即,使用了超文本的系统。可以把“超媒体”理解为超文本+媒体内容。

    REST是HTTP/1.1协议等Web规范的设计指导原则,HTTP/1.1协议正是为实现REST风格的架构而设计的。新的Web规范,其设计必须符合REST的要求,否则整个Web的体系架构会因为引入严重矛盾而崩溃。这句话不是危言耸听,做个类比,假如苏州市政府同意在市区著名园林的附近大型土木,建造大量具有后现代风格的摩天大楼,那么不久之后世界闻名的苏州园林美景将不复存在。

    上述这些关于“REST是什么”的描述,可以总结为一句话:REST是所有Web应用都应该遵守的架构设计指导原则。当然,REST并不是法律,违反了REST的指导原则,仍然能够实现应用的功能。但是违反了REST的指导原则,会付出很多代价,特别是对于大流量的网站而言。

    SpringMVC的REST风格请求

    在HTTP协议里面,有以下几个表示操作方式的动词:
    GET、POST、PUT、DELETE,分别对应着查询、增加、修改、删除操作。

    在SpringMVC中可以使用rest风格占位符的方式,使用注解@PAthVariable实现将URL中的数据复制到Java的具体变量上。

    比如以下:

    路径 HTTP方式 解释
    http://localhost:8080/stu/1 GET 获取id为1的数据(这里假设要取的是学生数据)
    http://localhost:8080/stu GET 获取所有的学生数据
    http://localhost:8080/stu/1 DELETE 删除id为1的学生的数据信息
    http://localhost:8080/stu/1 PUT 修改id为1的学生的数据信息
    http://localhost:8080/stu POST 新增一条学生数据

    对于以上的URL可以采用以下controller处理:

    GET

    对应URL:http://localhost:8080/stu 获取所有的学生的数据

    @responseBody//表示返回的JSON数据
    @RequestMapping(value="/stu",method=RequestMethod.GET)
    public Msg toAddPage(){//这里的Msg是自己封装的utils,封装了返回给client端的数据
        //获取到service端处理的数据
        return Msg.success().add("students",studentService.getStudents());
    }
    

    对应URL:http://localhost:8080/stu/1 获取学生id为1的学生数据

    POST

    对应URL:http://localhost:8080/stu 新增一条数据

    所有的数据都在formData中(可以F12查看网络请求),使用SpringMVC自动封装

    @responseBody//表示返回的JSON数据
    @RequestMapping(value="/stu",method=RequestMethod.POST)
    public Msg toAddPage(Student student){//这里的Msg是自己封装的utils,封装了返回给client端的数据
        studentService.saveStudent(student);
        return Msg.success();
    }
    

    PUT

    对应的URL:http://localhost:8080/stu/1 更新学生id为1的数据

    @responseBody//表示返回的JSON数据
    @RequestMapping(value="/stu{id}",method=RequestMethod.PUT)
    public Msg toAddPage(Student student){//这里的Msg是自己封装的utils,封装了返回给client端的数据
        studentService.updateStudentById(id);//此时student中是有id的,是SpringMVC封装好了的
        return Msg.success().add("student",student);
    }
    

    使用PUT、DELETE方法的时候,还需要注意一点就是,底层的Tomcat容器是只支持POST方式的,为了方便的使用rest风格的URL,在SpringMVC中提供一个过滤器:HiddenHttpMethodFilter,配置好此过滤器就可以使用PUT这些方法了。

    DELETE

    对应的URL:http://localhost:8080/stu/1 删除id为1的学生的数据

    @responseBody//表示返回的JSON数据
    @RequestMapping(value="/stu{id}",method=RequestMethod.PUT)
    public Msg toAddPage(@pathVariable(value="id"Integer id)){//这里的Msg是自己封装的utils,封装了返回给client端的数据
        studentService.deleteStudentById(id);
        return Msg.success();
    }
    
    阅读全文>>

作者:hello分类:【javaEE浏览(258评论(0

2020-12-15 11:12:55

spring-@Autowired注入与构造函数注入使用

    前言

    因为业务关系也看了些同事写的代码,因为公司没有明确规定,有一部分人在注入依赖的时候使用了用Spring推荐的构造器注入的方式,一部分人使用了@Autowired的注解进行注入。

    因此,接下来我试着总结归纳一下相关的情况,做一次回顾。

    用自己的话去说出来才算是真的掌握。

    使用介绍

    1.@Autowired注入

     

    1. @RestController
    2. @RequestMapping("/test")
    3. public class TestController {
    4. @Autowired
    5. private List<TestService> testServices;
    6. @Autowired
    7. private List<ChainAsbtract> chains;
    8. private ChainAsbtract target;
    9. }

    2.构造器注入

    Spring4.3+之后,constructor注入支持非显示注入方式。

    1. @RestController
    2. @RequestMapping("/test")
    3. public class TestController {
    4. // @Autowired
    5. private final List<TestService> testServices;
    6. // @Autowired
    7. private final List<ChainAsbtract> chains;
    8. // @Autowired
    9. public TestController(List<TestService> testServices, List<ChainAsbtract> chains) {
    10. this.testServices = testServices;
    11. this.chains = chains;
    12. }
    13. }

     3.setter注入

    1. @RestController
    2. @RequestMapping("/test")
    3. public class TestController {
    4. // @Autowired
    5. private final List<TestService> testServices;
    6. // @Autowired
    7. private final List<ChainAsbtract> chains;
    8. @Autowired
    9. public void setTestServices(List<TestService> testServices){
    10. this.testServices = testServices;
    11. }
    12. @Autowired
    13. public void setTestServices(List<ChainAsbtract> chains){
    14. this.chains = chains;
    15. }
    16. }

    事实上,spring在4.x版本后就推荐使用构造器的方式的来注入fileld

    官方推荐理由

    单一职责: 当使用构造函数注入的时候,你会很容易发现参数是否过多,这个时候需要考虑你这个类的职责是否过大,考虑拆分的问题;而当使用@Autowired注入field的时候,不容易发现问题

    依赖不可变: 只有使用构造函数注入才能注入final

    依赖隐藏:使用依赖注入容器意味着类不再对依赖对象负责,获取依赖对象的职责就从类抽离出来,IOC容器会帮你自动装备。这意味着它应该使用更明确清晰的公用接口方法或者构造器,这种方式就能很清晰的知道类需要什么和到底是使用setter还是构造器

    降低容器耦合度: 依赖注入框架的核心思想之一是托管类不应依赖于所使用的DI容器。换句话说,它应该只是一个普通的POJO,只要您将其传递给所有必需的依赖项,就可以独立地实例化。这样,您可以在单元测试中实例化它,而无需启动IOC容器并单独进行测试(使用一个可以进行集成测试的容器)。如果没有容器耦合,则可以将该类用作托管或非托管类,甚至可以切换到新的DI框架。

     另外,在使用构造器的使用能避免注入的依赖是空的情况。

    因为在bean的生命周期里面先执行的是bean的构造器,然后才给bean里面的属性赋值。具体内容在bean的生命周期里面,后面我学习之后按照自己的理解写写。

    阅读全文>>

作者:hello分类:【javaEE浏览(198评论(0

2020-11-30 11:09:15