分享一个扩展性在几年前还算可以的Listener?

大家好,这里是K字的研究.

最近整理硬盘, 翻到了很久之前写的一段代码, 现在是用不上了.不过,感觉放在硬盘里积灰可惜了, 拿出来分享下.一般性场景凑合一下估计也还能行.

先说说这个东西是干什么的.

有这么一个场景,  我监听了一个消息, 接收类型为A的参数. 后来, 又来了一个子类型为B的参数, 也想复用这个监听. 再后来又来了几个C,D,F子类型, 都想用这同一个, 但是每一个类型的处理函数又不太一样.

拆成N个监听器不是不可以,就是有点难受.


instanceof 大法

当年, 我们一开始的思路是这样的,不是有3个类吗?所有类实现共同的接口, 接收进来.

入口函数里通过接收基类接收进来, 一个一个instanceof来做.

 interface I{}
 class A implements I{} class B implements I{} class C implements I{}
 
 class Listener{
   void receive(I i){
     if(i instanceof A) dosth;
     if(i instanceof B) dosth;
     if(i instanceof C) dosth;
  }
 }
 
 class Handler{
   void handle(A a);
   void handle(B b);
   void handle(C c);
 }


不是不可以, 就是有点2. 每添加一个类型就要修改Handler,修改 Listener . 好像不太像正经人写的代码.而且子类越来越多, Handler类越来越长, 这也hold不住啊.

每个加一个类型就加一个Handler的实现

然后我们就改了, 先是把handler改成接口. 然后每新加一个类型, 就加一个Handler对应的实现类. 修改完的Handler变成了一个接口,以及他的实现族.

 interface Handler{
   void handle(I i);
 }
 
 class AHandler implements Handler{
   public void handle(I a){
     if(a instanceof A){
       A a1 = (A) a;
    }
  }
 }
 class BHandler implements Handler{
   public void handle(I b){}
 }

然后问题来了, 我都在AHandler里了, 参数居然还要再instanceof一次.

泛型大法好

后来我们找到了泛型法, 把Handler接口改成了这样.

 interface Handler<T extends I>{
  void handle(T i);
 }

实现类就不需要再写instanceof了.

 class AHandler implements Handler<A> {
   @Override
   public void handle(A i) {
  }
 }


类和Handler用配置绑定

Handler过长的问题解决了. 又有了新的问题, 我哪里知道每一个类型要用什么Handler啊?

这种关系型的东西, 需要用配置来绑定, 所以,我们写了一个enum来指定这个绑定.

 public enum Config {
   A_BIND(A.class, AHandler.class),
   B_BIND(B.class, BHandler.class);
 
   private final Class<Handler> tHandler;
   private final Class t;
 
   Config(Class t, Class tHandler) {
     this.t = t;
     this.tHandler = tHandler;
  }
 
   Class<Handler> find(Class<I> iClass) {
     return Arrays.stream(values()).filter(x -> x.t.equals(iClass))
      .findFirst()
      .map(x -> x.tHandler)
      .orElse(null);
  }
 }

有了这么一个可以通过参数的class类型来查询对应Handler的配置以后, 就可以改造Listener了.

 public class Listener {
  void receive(I i) {
    Class<Handler> handler = Config.find(i.getClass());
  }
 }

然后, 问题又来了. 这是一个Class类型, 不是Bean, 不能调用啊.

ApplicationContextAware来帮忙

不过不用怕, 有万能的Spring, 这就到了编程式使用SpringApplicationContext了.为了得到这个ApplicationContext,可以使用前几天提过的ApplicationContextAware接口扩展点.

改造完的长这样.

 public class Listener implements ApplicationContextAware {
   ApplicationContext context;
   
   void receive(I i) {
     Class<Handler> handler = Config.find(i.getClass());
     context.getBean(handler).handle(i);
  }
 
   @Override
   public void setApplicationContext(ApplicationContext  
                                     applicationContext)
                                 throws BeansException {
     context = applicationContext;
  }
 }


运行跑了好一阵子, 然后, 我们又遇到问题了… 有人会配置错Handler的对应关系, 让人头大.


我们的配置类, 容易配置错误. 换个说法, 他不防呆(Fool-proofing).


防呆

防呆是一个设计概念. 主要目的是降低使用者的犯错概率.

比如手机上的重置功能, 如果不小心点到, 会反复让你确认好几次, 是不是要抹掉手机全部内容. 甚至还有倒计时,几秒后开始抹除…提供给你一个反悔的时间.像是普通的别的危害不大的功能,很少有需要这么长步骤的.

这就是一个防呆功能. 如果不加防呆,比如把这个重置功能放到桌面上… 相信很多人会误触,失去手机里所有内容.

我们这个配置类, 他就不防呆,配置错了,也只有运行时候才能发现.编译器看不出来.

泛型, 还是泛型

通过泛型对构造器添加约束.可以做出来一个防呆版本的配置类.我们要求, 入参为T时候,Handler必须是Handler<T>.

改造完的版本如下:

 public enum Config {
   A_BIND(A.class, AHandler.class),
   B_BIND(B.class, BHandler.class);
 
   private final Class tHandler;
   private final Class t;
 
   <T extends I>Config2(Class<T> t, Class<? extends Handler<T>> tHandler) {
     this.t = t;
     this.tHandler = tHandler;
  }
 
  public static Class<Handler> find(Class<?extends I> iClass) {
     return Arrays.stream(values()).filter(x -> x.t.equals(iClass))
      .findFirst()
      .map(x -> x.tHandler)
      .orElse(null);
  }
 }

故意改错一个地方,编译器就开始报警了.

分享一个扩展性在几年前还算可以的Listener?

干! (DRY)

看起来是没啥大问题了,然而, 还有一个事,让人很在意.

AHandler implements Handler<A>

这个写法里面, 已经隐含了AHandlerA的关系了. 我们为什么要重复指定一遍配置呢?

计算机行业的老祖宗可是说过, !

哦, 不对是DRY, Don't Repeat Yourself.


抄袭Spring是我进步的阶梯

Spring以前就是需要配置的, 后来, 他做到了可以通过扫描包类确定配置关系.我们应该也可以.

用到两个技能点:

  1. ApplicationContext.getBeansOfType(Handler.class) 可以获取这个类型的所有Bean,包含子类.

  2. Type actualTypeArgument = ((ParameterizedType) entry.getValue().getClass().getGenericInterfaces()[0]).getActualTypeArguments()[0] 可以获取到泛型Bean上的泛型参数T

结合起来, 可以改造下代码, 我们来扫一扫,改造完就是这样的:

  void receive(I i) {
     Map<String, Handler> beansOfType = context.getBeansOfType(Handler.class);
     Handler bean = null;
     for (Handler value : beansOfType.values()) {
       Type actualTypeArgument = ((ParameterizedType) value.getClass().getGenericInterfaces()[0]).getActualTypeArguments()[0];
       if (((Class) actualTypeArgument).getName().equals(i.getClass().getName())) {
         bean = value;
         break;
      }
    }
     bean.handle(i);
  }

lazy的缓存

这代码仍然有一个问题, 每次进来都循环一次扫bean, 这肯定是不行的.要加一个缓存. 找到以后就放到缓存里. 改造完的代码是这样:

   Map<Class<? extends I>, Handler<I>> cache = new ConcurrentHashMap<>();
 
 void receive(I i) {
     Map<String, Handler> beansOfType = context.getBeansOfType(Handler.class);
     Handler bean = cache.computeIfAbsent(i.getClass(), x -> {
       for (Handler value : beansOfType.values()) {
         Type actualTypeArgument = ((ParameterizedType) value.getClass().getGenericInterfaces()[0]).getActualTypeArguments()[0];
         if (((Class) actualTypeArgument).getName().equals(i.getClass().getName())) {
           return value;
        }
      }
       return null;
    });
     bean.handle(i);
  }


看起来问题已经不大了. 但是他有另一个问题: 缓存穿透.如果一个类因为忘了或者别的什么原因,没有配置Handler.那么他的请求会反复的扫描bean.这是不能忍受的.

预热版本

这个对应关系,基本上就是固定的,启动以后就不会变.那么做成lazy的,其实没必要. 一启动就直接加载最好. 那么, 可以借助前阵子说过的@PostConstruct注解,来加载这个内容到cache.

改造好的版本是这样:

 
 public class Listener implements ApplicationContextAware {
   ApplicationContext context;
   Map<Class<? extends I>, Handler<I>> cache = new ConcurrentHashMap<>();
 
   void receive(I i) {
     Handler bean = cache.get(i.getClass());
     bean.handle(i);
  }
 
   @PostConstruct
   public void buildCache() {
     Map<String, Handler> beansOfType = context.getBeansOfType(Handler.class);
     for (Handler value : beansOfType.values()) {
       Type actualTypeArgument = ((ParameterizedType) value.getClass().getGenericInterfaces()[0]).getActualTypeArguments()[0];
       cache.put((Class) actualTypeArgument, value);
    }
  }
 
   @Override
   public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
     context = applicationContext;
  }
 }

这么着算是告一段落. Config配置类算是可以彻底删掉了. 如果要添加新的类型和对应的处理器. 自己去实现I接口,还有Handler接口就行. 其他的交由这段垃圾代码来控制. 是不是有点控制反转的意思了呢?    说了抄袭Spring,没白说吧.


输入部分的问题点

这段代码大概是好几年前为了接收一个消息队列搞得, 所以, 里面的返回值大多都是void.

今天为了简单起见, 又用A,B,C,D重写了一遍,顺便回顾了当年的演变改bug过程.  入口部分的子类,其实是一个json.

json转成子类的对象, 用了fastjson里最危险的功能,autotype.就是这几年经常出安全问题,导致fastjson几乎在业界上了黑名单的那个功能.比较😅,当时也是莽的厉害.

json示例
 JSON.toJSONString(new A(), SerializerFeature.WriteClassName)


 {
   "@type":"sh.now.afk.A"
 }


jsonparse回子类的代码.当年的版本比较简单,现在因为安全问题autotype被禁用了,所以要这么写,指定某个包下面还是可以autotype的.

  //设置一次就行
  ParserConfig.getGlobalInstance().addAccept("sh.now.afk");
 
 //其他地方就可以正常用了
 I i = JSON.parse("{ "@type":"sh.now.afk.A"}");
 

不过即使是开了autotype白名单, 仍然会有意想不到的问题. 建议还是不要使用这个功能.

JSONPath取出来类名, 然后用复杂一点的接口这么写也是可以的:

 String type = JSONPath.extract(x, "\@type")+"";
 try {
       I o = (I) JSON.parseObject(x, Class.forName(type));
 } catch (ClassNotFoundException e) {
   e.printStackTrace();
 }


总结

这篇比较水, 主要就是从硬盘里翻了点东西出来改吧改吧. 不过因为用到的知识点不多不少刚刚好是一篇文章能说明的量,其实还是可以拿出来看看的.

  1. 用到了Spring 相关的操作知识,包括Bean操作, PostConstruct

  2. 泛型和泛型extends

  3. 防呆设计和DRY法则

  4. 如何从泛型类上取出具体的泛型参数 // 这个是核心,没有这个我们配置类去不掉

  5. fastjson的autotype和jsonpath

  6. 缓存穿透


我是jkl, 一个正在整理东西的程序员.

给这期加了个新标签,源码解析. 不过解析的是自己以前写的源码. 回头再解析点别人的源码吧,争取做成一个系列. 文章里的示例源码是写文章时候随手重写的, 有不少问题.不过今天就这样吧.来日方长

原文始发于微信公众号(K字的研究):分享一个扩展性在几年前还算可以的Listener?

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/24594.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!