采用Spring Boot Event 实现业务解耦

792 阅读4分钟

我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第4篇文章,点击查看活动详情

前言

实际业务开发程中,实现复杂的业务逻辑,如果将核心业务逻辑和非核心业务耦合一起,代码的耦合度高,维护起性和扩展性很差,所以需要进行解耦,通常大家会联想到MQ,MQ虽然可以解决业务解耦问题,但是也会提升架构的复杂度,那么有没有简单的解决方案呢?今天给大家讲解下,如何使用Spring Boot Event来实现业务解耦。

具体实现

定义Event事件

事件是发送的消息体,可以根据实际的业务进行封装不同的数据信息。注意传递参数需要重写父类的构造方法。例如创建发送邮件事件。

public class EmailEvent extends ApplicationEvent
{
    private static final long serialVersionUID = -2098387871131250466L;
    
    private TUser tUser;
    
    public EmailEvent(TUser tUser)
    {
        super(tUser);
        this.tUser = tUser;
    }

    public TUser gettUser()
    {
        return tUser;
    }
}

注意:传递参数需要重写父类的构造函数。

定义监听器

监听器相当于MQ中的消费者,当有相关的事件推送过来,监听器能够监听执行相关的业务处理。

接口实现监听器

@Component
public class EmailListener implements ApplicationListener<EmailEvent>
{
    private static final Logger logger = LoggerFactory.getLogger(EmailListener.class);
    
    @Async
    @Override
    public void onApplicationEvent(EmailEvent event)
    {
        TUser tUser = event.gettUser();
        logger.info("tuser:{}",JSON.toJSONString(tUser));
        //省略发送邮件的逻辑实现
    }
}

说明:监听器需要添加@Component注解将其注入到Spring容器中。

事件发布

发布事件非常简单,只需要调用ApplicationEventPublisher.publishEvent来发布事件.

@RestController
@RequestMapping("/event")
public class EventController
{
    private Logger logger = LoggerFactory.getLogger(EventController.class);
    
    @Autowired
    private ApplicationEventPublisher applicationEventPublisher;
    
    @RequestMapping("/addUser")
    public void addUser()
    {
        logger.info("send email start");
        
        TUser tuser =new TUser();
        tuser.setOid(1009l);
        tuser.setName("测试事件");
        //省略新增用户逻辑        
        //发送邮件
        EmailEvent emailEvent =new EmailEvent(tuser);
        applicationEventPublisher.publishEvent(emailEvent);
        logger.info(" send email end");
    }
}

测试结果

c.s.f.c.ThreadPoolExecutorMdcWrapper - Initializing ExecutorService
c.skywares.fw.event.EventController -  send email end
c.s.f.c.exception.ResponseAdvice - before body write param:null
com.skywares.fw.event.EmailListener - tuser:{"age":0,"name":"测试事件","oid":1009}

从测试结果来看,事件已经执行成功。

注解方式实现

除了通过实现接口来实现监听器,我们可以采用@EventListener注解的方式来实现,具体实现如下。

发布事件

@Component
public class EmailListenerPublisher 
{
    private static final Logger logger = LoggerFactory.getLogger(EmailListenerPublisher.class);
    
    @Async
    @EventListener(EmailEvent.class)
    public void publisher(EmailEvent event)
    {
        TUser tUser = event.gettUser();
        logger.info("Event Listener{}",JSON.toJSONString(tUser));
    }
}

说明:采用@EventListener注解方式来实现,需要指定事件的类型。

相关测试

  @Autowired
  private EmailListenerPublisher emailListenerPublisher;
    
@RequestMapping("/publisher")
    public void publisher()
    {
        logger.info("send email start");
        
        TUser tuser =new TUser();
        tuser.setOid(1008l);
        tuser.setName("注解方式测试事件");
        
        //发送邮件
        EmailEvent emailEvent =new EmailEvent(tuser);
        emailListenerPublisher.publisher(emailEvent);
        logger.info(" send email end");
    }

说明:执行结果与接口实现法式一致。

异步执行事件

默认情况事件是同步执行,需要采用异步执行方式,需要在监听器方法上添加@Async注解即可。

监听器上添加@Async注解

@Component
public class EmailListener implements ApplicationListener<EmailEvent>
{
    private static final Logger logger = LoggerFactory.getLogger(EmailListener.class);
    
    @Async
    @Override
    public void onApplicationEvent(EmailEvent event)
    {
        TUser tUser = event.gettUser();
        logger.info("tuser:{}",JSON.toJSONString(tUser));
    }
}

注意: @Async默认使用的线程池最大线程数是Integer.MAX,并且阻塞队列的大小也是Integer.MAX, 采用默认方式容易产生OOM异常,所以不建议采用默认线程池,需要根据业务的情况自定义线程池。

自定义异步线程池

@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer
{
   private final Logger logger = LoggerFactory.getLogger(this.getClass());
   
  public static final int PROCESSORS = Runtime.getRuntime().availableProcessors();
    
    @Override
    public Executor getAsyncExecutor()
    {
        //自定义线程池
        ThreadPoolExecutorMdcWrapper taskExecutor =new ThreadPoolExecutorMdcWrapper();
        //核心线程数
        taskExecutor.setCorePoolSize(PROCESSORS*2);
        //最大线程数
        taskExecutor.setMaxPoolSize(PROCESSORS*4);
        //队列大小
        taskExecutor.setQueueCapacity(500);
        taskExecutor.setKeepAliveSeconds(60);
        //线程名称
        taskExecutor.setThreadNamePrefix("async-thread-");
        taskExecutor.initialize();
        return taskExecutor;
    }
    
    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler()
    {
        return  new CustAsyncExceptionHandler();
    }
    
     class CustAsyncExceptionHandler implements AsyncUncaughtExceptionHandler {
        @Override
        public void handleUncaughtException(Throwable ex, Method method,
                Object... params)
        {
           logger.error("Unexpected exception occurred invoking async method: " + method, ex);
        }
    }
}

说明:ThreadPoolExecutorMdcWrapper为自定义线程的实现。

事件机制优缺点

优点

降低代码的耦合度

通过不同的事件,将业务逻辑解耦,只需要发布相关事件,不需要关注具体的实现逻辑,业务逻辑清晰,便于后续的维护和扩展。

增强代码的复用度

相同的非核心业务功能,只需要定义相关事件和实现,其他的模块都能进行复用,增强了代码的复用度。

缺点

SpringBoot的事件只能在单系统中使用,无法跨系统调用。

总结

本文对于Spring Boot Event的事件进行相关讲解,针对不同的业务选择合适的方案,如有疑问请随时反馈。