项目中应用eventbus解决的问题

在项目开发过程中,往往有些功能表面看起来简单,但实际开发的结果非常复杂,仔细分析下原因发现很多都是因为附加了许多的额外功能。

真的简单吗?

比如我们对一个电商平台的商品数据做修改的功能来讲,其实非常简单,无非就是运营人员在管理平台中对商品进行修改数据,然后点击提交,核心功能的确很简单,但可能有人会要求对商品的修改都需要增加操作日志,还有人提出需要在商品数据修改后自动去更新检索系统中的数据,有人提要在商品数据修改后需要经过审核人的审核才能生效,还有人提需要给运营人员发邮件通知等等,如此一来这个商品修改的功能就不是那么简单了,它除了完成自己的使命,还需要去调用其它的服务来完成,活动类似如下:

横向的主流程,上下四个小方框是附加功能,复杂的原因包含如下两点:

  • 附加功能导致功能开点变多,工作量加大
  • 附加功能导致程序逻辑复杂,在程序中需要去访问其它的服务,还需要考虑数据完整性,性能,服务依赖等各种问题。

刚开始我们在项目中开发时,使用的就是最简单的强耦合去直接调用服务,比如在调用完数据保存的方法后,去调用logService的log方法记录日志,调用esService的方法去更新检索系统,调用mailService的方法去发邮件,这样会导致我们的productService强耦合这些与商品保存逻辑没有直接关联的服务,这样看起来商品保存的功能变得不那么单纯,也就是我们文前提到的复杂了,会出现类似代码:

@Autowired
private SearchService searchService;
@Autowired
private MailService mailService;
@Autowired
private ProductLogService productLogService;

//如下下保存商品的代码片段
itemDao.save(product);
searchService.update(product.getId());
mailService.post(product.getId());
logService.log(product.getId());

问题如下:

  • 对无业务直接关联的服务强耦合
  • 可能存在性能问题,比如你需要关心邮件发送是否会影响主流程
  • 代码可读性变差,过多的逻辑容易导致分不清主体核心功能

如果解决呢?有一个设计模式可以解决,那就是观察者模式,之前学习.net时专门写过一篇(老生常谈:观察者模式),里面提到有传统的实现方式以及事件机制,事件机制的实现比较简单一些,这里我们在解决这个问题时引用了guava组件中提供的eventbus,它与之前那篇观察者模式的实现很相似,看下面这张图,功能之间没有错综复杂的依赖。

注:guava的eventbus是个进程内级别的,无法跨进程,后面我抽时间再整理下基于消息队列的分布式事件总结。

这里我并不介绍如何使用EventBus,而是重点来说明我们项目中对它的应用,哪些是做的不好的地方。

第一:要有观察者,这里我们根据不同的业务封装不同的观察者,下面是更新检索系统数据的类

@Service
  public class SearchEventListener {
    @Autowired()
    ProductUpdateMgr productSearchUpdateMgr;
    private final static Logger logger = LoggerFactory.getLogger(SearchEventListener.class);
    @Subscribe
    public void listen(String itemLegacyId) {
        try
        {
            productSearchUpdateMgr.markProductDirty(itemLegacyId);

        }
        catch(Exception ex)
        {
            logger.error("更新检索异常:" + ex.getMessage() + ex.getStackTrace());
        }
    }
}

第二:需要有将观察者注册到eventbus中去,我们专门写了一个类来做这件事情,完成两件事情:

  • 注册观察者到eventbus中
  • 进一步包装post方法以便调用者以服务形式调用,让productService依赖eventbus而不是依赖实际的检索服务,邮件服务等
@Service
public class EventListenerManager {
    EventBus mmsProductEventBus;
    EventBus mmsItemEventBus;
    EventBus mmsSearchEventBus;
    EventBus mmsRebuildAllSearchEventBus;
    EventBus mmsCommonLogEventBus;
    @Autowired
    ItemEventListener itemListener;
    @Autowired
    ProductEventListener productListener;
    @Autowired
    SearchEventListener searchListener;
    @Autowired
    RebuildAllSearchEventListener rebuildAllSearchListener;
    @Autowired
    MmsCommonLogEventListener commonLogListener;

    @PostConstruct
    private void init() {
        mmsProductEventBus = new EventBus();
        mmsItemEventBus = new EventBus();
        mmsSearchEventBus = new EventBus();
        mmsRebuildAllSearchEventBus=new EventBus();
        mmsCommonLogEventBus=new EventBus();
        mmsItemEventBus.register(itemListener);
        mmsProductEventBus.register(productListener);
        mmsSearchEventBus.register(searchListener);
        mmsRebuildAllSearchEventBus.register(rebuildAllSearchListener);
        mmsCommonLogEventBus.register(commonLogListener);
    }

    public void notifyItemLog(Long itemId) {
        mmsItemEventBus.post(itemId);
    }

    public void notifyProductLog(Long productId) {
        mmsProductEventBus.post(productId);
    }

    public void notifyUpdateSearch(String itemLegacyId) {
        mmsSearchEventBus.post(itemLegacyId);
    }
    public void notifyRebuildAllSearch() {
        mmsRebuildAllSearchEventBus.post("");
    }
    public void notifyAddCommonLog(MmsCommonLogModel log) {
        mmsCommonLogEventBus.post(log);
    }
} 

上面的实现以及我们在刚学习使用guava eventbugs时遇到哪些问题呢?
  问题一:为什么上面的代码有这么多的eventbus而不是一个呢?注意下enventbus的post方法,我们再看下它的源码:它是根据参数的类型来找观察者注册的方法的,而我们写的观察者类中的方法中的参数都是一些primitive类型的,总共有10个左右方法,要想根据参数类型来正确的在一个eventbus中识别调用哪个方法,是比较困难的。

public void post(Object event) {
    Set<Class<?>> dispatchTypes = flattenHierarchy(event.getClass());

    boolean dispatched = false;
    for (Class<?> eventType : dispatchTypes) {
      subscribersByTypeLock.readLock().lock();
      try {
        Set<EventSubscriber> wrappers = subscribersByType.get(eventType);

        if (!wrappers.isEmpty()) {
          dispatched = true;
          for (EventSubscriber wrapper : wrappers) {
            enqueueEvent(event, wrapper);
          }
        }
      } finally {
        subscribersByTypeLock.readLock().unlock();
      }
    }

    if (!dispatched && !(event instanceof DeadEvent)) {
      post(new DeadEvent(this, event));
    }

    dispatchQueuedEvents();
  }

如何解决?可以针对每个方法的参数封装一个类,比如更新检索的方法参数叫SearchChangeEvent,发送审核邮件的参数叫ApprovalChangeEvent等等,这样我们就可以将所有的观察者注册到一个eventbus中,调用post方法时就不会出现问题了,最后简化后的结果如下:

@Autowired
EventListenerManager eventManager;

//如下下保存商品的代码片段
itemDao.save(product);
eventManager.post(new SearchChangeEvent(product.getId));
eventManager.post(new MailChangeEvent(product.getId));
eventManager.post(new LogChangeEvent(product.getId));

问题二:性能问题,之前有提到过附加的功能会导致原本单纯的事物不单纯,比如调用某些服务时可能影响整体性能,当时我们想当然的认为使用了enventbus本身就是异步的,其实如果用eventbus它是同步的,要想使用异步需要使用这个类来完成AsyncEventBus。

问题三:强耦合问题,由于有了EventListenerManager,我们在具体业务中就不需要依赖不直接相关的服务了,只需要依赖EventListenerManager这个看起来与任务业务都无关的管理类就可以了。


  通过实际项目中对eventbus的应用来分析它能解决的问题以及当初应用有待提高的地方。很显示eventbus应用得当可以简化程序复杂性,提高代码可读性,降低开发维护成本。

时间: 2024-08-24 08:47:44

项目中应用eventbus解决的问题的相关文章

项目中阶梯费率解决方法,数组中通过键名查找键值

<?php $a = 3.5;$arr = array("1"=>4,"2.5"=>5,"5"=>6);//小数做键名,需要加引号$arr = array_flip($arr); foreach ($arr as $key => $value) { $b[] = $value;} 主要思路就是把数组翻转,取出原数组所有键名组成一维数组,判断出在哪个阶梯范围,再去反转的数组中array_search出对应的键值,这个键

loadrunner解决在项目中的难点解决

代码如下: vuser_init() { lr_save_string("11041331\",\"11041372\",\"11041373\",\"11041374","OrderNo"); lr_output_message("%s", lr_eval_string("{OrderNo}")); return 0; } Action() { int i=0,j;

项目中遇到的某些问题及解决办法(一)

简介 该博文记录了一些平时在工作中遇到的问题及解决办法,某些问题有解决办法,某些问题暂时没有解决办法,如果有大神知道的,请多多指点. 如果某些问题有更好的解决办法,也请指教. 正文 1.在一个方法中用泛型操作两个不同的类型(Type). 难点:需要实现一个方法,进入参数一个泛型,返回信息一个泛型.但是一个方法中泛型只支持一种类型. 解决办法:将进入和返回放在一个类型中,用特性将进入参数和返回参数区分开. 2.微信三方登录,需要在PC桌面应用端+API服务实现. 难点:微信官网只提供了网页三方登录

同一个项目中存在完全相同的包名和类名如何解决调用问题

项目中遇到有一个类,在两个jar包中都存在,而且类所在的包名和类名完全一致,解决办法有两种: 1.常用办法 清除项目中过时的那个jar包,推荐方式. 2.如果两个都不能清除,则在使用过程中动态指定加载的jar包即可.以rt.jar中javax.xml.ws.Service为例,代码如下 File file = new File("f:\\rt.jar"); URL url = file.toURI().toURL(); ClassLoader classLoader = new URL

项目记录:spring+springmvc 项目中 @Transactional 失效的解决方法

第一步,修改spring的配置文件和springmvc的配置文件 --------------------------------applicationContext.xml <context:annotation-config/>  <context:component-scan base-package="com.xxx"> <context:exclude-filter type="annotation" expression=&

新建解决方案 在解决方案中添加项目中,解决方案消失的解决办法

新建空白解决方案的步骤:文件--新建项目--其他项目类型--Visual Studio 解决方案 这样就建立出了一个空白解决方案. 然后在资源管理器中可以添加项目,但是添加项目的时候会发现,解决方案消失了,解决办法  工具--选项--项目和解决方案(如果看不到这个,在下方有一个显示所有设置打勾),然后右边有一个 总是显示解决方案.勾上,解决方案就出现了 记录一下. 新建解决方案 在解决方案中添加项目中,解决方案消失的解决办法,布布扣,bubuko.com

现实项目中用户随意添加序号,如何用SQL解决序号连续性问题

前段时间,一直忙于学习golang语言,没有时间整理项目中用到的方法,今天趁着有空写下笔记. 项目中,遇到一个比较"刁钻"的需求:用户用Excel导入到系统里,每一行前面都有一个序号,序号分成两部分,如下所示: 左边部分是大序号,右边是小序号,类似于书籍目录那样,序号是由用户自己编写,而且用户可以随意在Excel序号插入任何新序号,用户不保证新增或者编辑的序号是否正确,我们要做的是检查这些序号. 以下是我的检查思路: 1.序号是否连续 我们要事先给用户做一个限制,在大序号后面添加小序号

解决android studio项目中Failded to sync Gradle project &#39;XXXX&#39; Cause:failed to find target with hash string &#39;android-16&#39;问题

之前在github上通过import module导入一个项目,结果报错,提示找不到sdk相应的版本xx,而我的compileSdkVersion明明写的是23不是xx,查了半天也没解决.最后只好下载了那个版本的sdk. 今天导入SlidingMenu的module的时候,又遇到了这个问题.  问题: Cause:failed to find target with hash string 'android-16' in: E:\sony\Android\sdk failed to find B

WCF项目中出现常见错误的解决方法:基础连接已经关闭: 连接被意外关闭

原文:WCF项目中出现常见错误的解决方法:基础连接已经关闭: 连接被意外关闭 在我们开发WCF项目的时候,常常会碰到一些莫名其妙的错误,有时候如果根据它的错误提示信息,一般很难定位到具体的问题所在,而由于WCF服务的特殊性,调试起来也不是那么方便,因此往往会花费不少时间来进行跟踪处理.本文介绍我在我在我的框架里面使用WCF服务的时候,出现的一个常见错误的处理方法,它的提示信息是:基础连接已经关闭: 连接被意外关闭.这种情况我碰到的有两种,一种是返回DataTable的时候出现的,一种是返回实体类