大家好,这里是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
, 这就到了编程式
使用Spring
的ApplicationContext
了.为了得到这个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);
}
}
故意改错一个地方,编译器就开始报警了.
干! (DRY)
看起来是没啥大问题了,然而, 还有一个事,让人很在意.
AHandler implements Handler<A>
这个写法里面, 已经隐含了AHandler
和A
的关系了. 我们为什么要重复指定一遍配置呢?
计算机行业的老祖宗可是说过, 干
!
哦, 不对是DRY
, Don't Repeat Yourself
.
抄袭Spring是我进步的阶梯
Spring以前就是需要配置的, 后来, 他做到了可以通过扫描包类确定配置关系.我们应该也可以.
用到两个技能点:
-
ApplicationContext.getBeansOfType(Handler.class)
可以获取这个类型的所有Bean,包含子类. -
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"
}
从json
parse回子类的代码.当年的版本比较简单,现在因为安全问题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();
}
总结
这篇比较水, 主要就是从硬盘里翻了点东西出来改吧改吧. 不过因为用到的知识点不多不少刚刚好是一篇文章能说明的量,其实还是可以拿出来看看的.
-
用到了Spring 相关的操作知识,包括Bean操作, PostConstruct
-
泛型和泛型extends
-
防呆设计和DRY法则
-
如何从泛型类上取出具体的泛型参数 // 这个是核心,没有这个我们配置类去不掉
-
fastjson的autotype和jsonpath
-
缓存穿透
我是jkl, 一个正在整理东西的程序员.
给这期加了个新标签,源码解析. 不过解析的是自己以前写的源码. 回头再解析点别人的源码吧,争取做成一个系列. 文章里的示例源码是写文章时候随手重写的, 有不少问题.不过今天就这样吧.来日方长
原文始发于微信公众号(K字的研究):分享一个扩展性在几年前还算可以的Listener?
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/24594.html