最早接触责任链这个设计模式,是我老早前看 Spring Aop 的源码的时候,Aop 的原理是遍历一根按照顺序装载好的 Advice(通知)拦截器链条,使@Before、@After 这些 Advice(通知)中的逻辑有顺序执行。如果我们没有 @After 的需要 ,Spring 只需把拦截器链条中的 @After 这个节点去掉就是了。十分的方便快捷!最近在看京东旗下的一个开源项目 hotkey 一个内存消息中间件,还有码云的一个 GVP 项目 austin 一个消息推送平台,里面用到了很多设计模式,不乏责任链的使用。当时看的时候一遍就过掉了,然后发现好像自己平时也没有总结过责任链这个设计模式,于是乎本文出现了。
要知道责任链的使用场景有哪些就要先说下责任链的核心之处了:
业务一:前置参数校验、接口逻辑执行、接口后置返回结果处理,这套业务就可以用责任链实现,还譬如业务二:审批流程:a 发起、b 抄送、c 审核、d 审核、e 审定。这种业务也可以用到责任链。只要是线性业务很多地方都可以用到责任链。假设业务一现在新增需求加入权限校验的功能,如果整体系统的接口请求都是走的业务一这套流程,加个权限校验无非就是在业务一责任链尾节点加个权限校验的节点就好了,其他业务逻辑根本不用变。如果没使用责任链的情况下是不是每个接口都需要做个判断(当前用户是否有权限访问该接口)。当然市面上有很多权限的框架、拦截器技术来实现等等,我这里只是介绍责任链设计模式的思想。就拿业务一来用责任链来实现的代码又是怎样的呢?请看下文
责任链无非就是按照顺序遍历执行其中的节点而已,那么责任链节点是什么时候塞进去的呢?常见的方式有俩种:一种是项目启动之初赋予默认节点、还有一种是用到的时候手动赋予节点。那么接下来就开始手撸代码。利用 @Resource 注解寻找 IOC 容器中的类型为(LoginInterceptor)的 bean 装配赋予 interceptors,可能有人会问为什么不用 @Autowired 呢?@Autowired 只适用于 Spring 环境,@Resource 除了适应 Spring 环境还可适应别的容器。由于我本地用的 Spring 这里用 @Resource、@Autowired 都行。另外还提供了一个 addInterceptors 方法,就是对应 interceptors 的 set 方法改了个名字而已,大家可以根据需要手动装配节点。
@Component
public class LoginInterceptorHandler {
/**
* 自动装配 ioc 容器中的类型为(LoginInterceptor)的 bean 链条 ,也可用 @Autowired 替代
*/
@Resource
private List<ILoginInterceptor> interceptors;
public Boolean chain(LoginContext loginContext) {
for (ILoginInterceptor interceptor : interceptors) {
Boolean doNext = interceptor.invoke(loginContext);
//责任链某一环不执行了,后续节点都不会执行
if (!doNext) return doNext;
}
return true;
}
/**
* 提供扩展:手动配置生效的链条
*/
public void addInterceptors(List<ILoginInterceptor> interceptors) {
this.interceptors = interceptors;
}
}
由于责任链的节点是按某一类型的 Bean 进行装配的,但是每个节点的业务又都不一样,换句话说就是业务具有多态性,而责任链节点类型具有唯一性。因此我们定义一个 ILoginInterceptor 类型的接口,提供给每个节点实现。以下代码就是一个简单的参数校验节点。值得注意的是我这里用到了 @Order(1) 注解,值越小越先放到 IOC 容器,越先放到 IOC 中的越先被责任链装配,因此 @Order(1) 中的这个 1 代表责任链的头节点。
//值越小,在责任链中的排序越前
@Order(1)
@Slf4j
@Component
public class CheckParamInterceptor implements ILoginInterceptor {
/**
* 校验参数
*/
@Override
public Boolean invoke(LoginContext loginContext) {
switch (loginContext.getType()) {
//短信验证码
case "1": {
Assert.notBlank(loginContext.getCode(), "短信验证码不能为空");
Assert.notBlank(loginContext.getUserName(), "用户名不能为空");
Assert.notBlank(loginContext.getPassword(), "密码不能为空");
if ("sucessCode".equals(loginContext.getCode())) throw new RuntimeException("短信验证码错误");
break;
}
//用户名密码
case "2": {
Assert.notBlank(loginContext.getUserName(), "用户名不能为空");
Assert.notBlank(loginContext.getPassword(), "密码不能为空");
break;
}
//微信
case "3": {
Assert.notBlank(loginContext.getWxCode(), "微信授权码不能为空");
break;
}
default:
break;
}
return true;
}
}
//值越小,排序越前
@Order(2)
@Slf4j
@Component
public class DoLoginInterceptor implements ILoginInterceptor {
/**
* 真正的登录逻辑
*/
@Override
public Boolean invoke(LoginContext loginContext) {
switch (loginContext.getType()) {
//短信验证码
case "1": {
this.simpleLogin(loginContext);
break;
}
//用户名密码
case "2": {
this.simpleLogin(loginContext);
break;
}
//微信
case "3": {
this.wxLogin(loginContext);
break;
}
default:
break;
}
return true;
}
public void simpleLogin(LoginContext loginContext) {
if (!("zzh".equals(loginContext.getUserName()) && "zzhpwd".equals(loginContext.getPassword())))
throw new RuntimeException("用户名或者密码错误");
JSONObject jsonObject = new JSONObject();
jsonObject.put("uid", "21422");
jsonObject.put("userName", loginContext.getUserName());
jsonObject.put("password", loginContext.getPassword());
loginContext.setUserInfo(jsonObject);
}
public void wxLogin(LoginContext loginContext) {
//后续执行:通过微信 code 获取 token 进而获取微信用户信息,用户信息同步到本地的逻辑......
JSONObject jsonObject = new JSONObject();
jsonObject.put("uid", "21422");
jsonObject.put("userName", loginContext.getUserName());
jsonObject.put("password", loginContext.getPassword());
jsonObject.put("wxCode", loginContext.getWxCode());
loginContext.setUserInfo(jsonObject);
}
}
由于本文模拟的是登录逻辑,触发时机当然是在 Controller 中触发的了,注入责任链处理器,调用里面的 chain 方法即可。这样一来参数校验的业务、登录查 db 的业务就被我们组件化抽离开来了。
@Autowired
private LoginInterceptorHandler loginInterceptorHandler;
@PostMapping(value = "testchain")
public R testchain(@RequestBody LoginContext loginContext) {
Boolean chain = loginInterceptorHandler.chain(loginContext);
if (!chain) return R.failed("未知错误");
//登录成功返回用户信息
return R.ok(loginContext.getUserInfo());
}
这里提一嘴体外话:市面上有很多优秀的开源框架例如 Spring Aop 切面无非就是使用 JDK/CGLIB 代理+责任链实现的,而责任链里面装配了一个个 Advice 也就是我们常说的 @Before、@After 通知,当我们调用目标方法的时候会先执行我们的 @Before 增强逻辑,因为 @Before 增强逻辑节点排在了真正方法逻辑节点的前面。下面贴一段 Aop 责任链触发的源码,感兴趣的小伙伴可以阅读 aop增强器执行流程,手把手带你debug (文章末尾有干货)
而 hotkey 这个开源消息中间件是在 Netty 的 channelRead0 这个监听方法里面触发的责任链逻辑。具体怎么触发不重要根据各自的业务逻辑来即可。
参数错误是触发提示
正常登录
加入权限校验节点登录提示无权限
@Slf4j
@Order(3)
@Component
public class VerifyPermissionInterceptor implements ILoginInterceptor {
@Override
public Boolean invoke(LoginContext loginContext) {
Object userInfo = loginContext.getUserInfo();
//根绝 db 用户信息,解析对应的权限
throw new RuntimeException("无权限访问系统");
}
}
本文模拟的是比较简单也通用的责任链,可能很多小伙伴也看 Spring Aop的源码,发现里面的那个责任链和我本文写的不同,他那个写的比较复杂由于要兼顾到 @AfterThrowing 通知,他顺序遍历了一遍责任链后,又从责任链尾节点倒叙遍历了一遍责任链,而本文的这种只进行一遍顺序遍历责任链,也够应对日常开发了。具体 Spring Aop 是怎么遍历可以阅读 aop增强器执行流程,手把手带你debug (文章末尾有干货)
读的是软件工程这个专业,内心却怀揣着一颗创作的心。对于敲代码而言我是业余的,但是写作我是认真的