你开发了一套博客系统,但用户老发一些涉黄、广告词汇。眼看网站就要被封了,你该怎么办?
对的,过滤掉敏感词,还广大用户一篇清静。
实现这个功能,你很自然地想到 Servlet 的过滤器,或者是 Spring 的拦截器。
然而,你有想过吗?框架中的过滤器、拦截器是怎么实现的呢?
虽然我们没机会开发一套拦截器机制,但了解开源框架的底层实现,会大大提高我们的敲代码水平。再不济,你也能在同事面前装下X。
责任链模式的原理和实现
拦截器的实现,离不开责任链模式。 无论是 Spring 的拦截器,还是 Tomcat 实现的 Servlet 过滤器,都用到了责任链模式。
我们先来看它的官方定义:将请求的发送和接收解耦,让多个接收对象都有机会处理这个请求。将这些接收对象串成一条链,并沿着这条链传递这个请求,直到链上的某个接收对象能够处理它为止。
看完之后,你一脸懵逼。简单来说,你定义了多个处理器,依次处理同一个请求。
比如说,一个http请求,先经过A处理器处理,再传给B处理器,然后再传给C处理器,就这样一直传下去,形成一条链条。在这条链条上,每个处理器的都承担自己的责任,所以叫做:责任链模式。
这就是责任链模式的原理了。那代码怎么写?
思路是这样的。我们先定义两个处理器类(HandlerA、HandlerB),来处理具体的业务。再定义一个处理器链条,负责不断调用处理器,处理客户的请求。最后,创建一个处理器链条,这就是我们的客户端代码。
你可以结合下面的代码,加深一下理解。
import java.util.ArrayList;
import java.util.List;
/**
* @Author: jiarupc
* @Date: 2020/9/28
* @Description: 责任链模式-演示demo
*/
public class ChainOfResponsibility {
/*********************** 处理器接口及实现 *************************/
public interface IHandler {
boolean handle(String request);
}
public static class HandlerA implements IHandler {
@Override
public boolean handle(String request) {
System.out.println("已经过A处理器处理");
// ...你想要的业务
return request != null && !"".equals(request);
}
}
public static class HandlerB implements IHandler {
@Override
public boolean handle(String request) {
System.out.println("已经过B处理器处理");
// ...你想要的业务
return request != null && !"".equals(request);
}
}
/*********************** 处理链条 *************************/
public static class HandlerChain {
private List<IHandler> handlers = new ArrayList<>();
public void addHandler(IHandler handler) {
this.handlers.add(handler);
}
public void handle(String request) {
for (IHandler handler : handlers) {
boolean isHandle = handler.handle(request);
if (!isHandle) {
System.out.println("处理失败,跳过链条,直接退出");
break;
}
}
System.out.println("处理完成,共调用 " + handlers.size() + " 个处理器");
}
}
/*********************** 客户端 *************************/
public static void main(String[] args) {
// 构建处理器链条,你可以用注解和反射,实现自动发现处理器
HandlerChain chain = new HandlerChain();
chain.addHandler(new HandlerA());
chain.addHandler(new HandlerB());
// 启动处理链,处理request
String request = "请求模拟";
chain.handle(request);
}
}
为什么要用责任链模式?
拿文章开头的业务做例子,我们现在不但要过滤涉黄词汇,还要过滤替换一些特殊符号。
这时候,我们可以直接改拦截器的代码,直接在上面多写几个 if-else 判断。在小项目上,这样完全没问题。但要是碰上大项目,需求多了,你的代码很可能会越写越多,越写越乱。
比方说,除了过滤涉黄、广告等词汇,还得过滤替换特殊符号。要做的东西越来越多,你能在里面无限写下去吗?这时候该怎么办?
除此之外,在大项目、开源项目,是很多人、很多团队同时开发的,工作内容明确,泾渭分明。别人没法改你的代码,你必须给别人的开发留足空间。
责任链模式能提高代码的拓展性。
你不用再改别人的代码了,只要再新建一个 Handler 类,实现它的 handle() 方法,再调用 addHandler() 方法,添加进 HandlerChain,这就可以了。
你想想,Spring 的拦截器不也是这样吗?先自定义一个 intercepter,再加上注解,就能拦截处理http请求了。
这些各种高大上的框架,之所以用起来这么顺手,是因为给你留足了空间,让你能按自己的需求取开发。而这一切的开始,就是在上面的简单 demo 中,一步步迭代出来的。
Shiro 怎么添加过滤器?
责任链模式经常出现在框架中,让我们在不修改源码的情况下,添加新的功能。
Shiro 是一个Java安全框架,提供了各种安全认证功能。这种安全框架一般会用各种过滤器,来实现权限拦截功能。
我们就从 Shiro 的入手,来看看这些高大上的项目,是怎么运用责任链模式的。
Shiro 在 Servlet Filter 的基础上,自己定制了一套的过滤器功能。就是下面这副图:
Shiro-拦截器流程在开发的时候,我们该怎么使用 Shiro Filter 呢?首先,新建一个自己的 Filter 类,并且继承 UserFilter。然后,把刚才新建的 Filter 类,加到配置文件就行了。
// 这里只留下了过滤器的代码,删掉了其它的代码,大家可以看得更直观
/**
* @Author: jiarupc
* @Date: 2020/8/26
* @Description: 登录token过滤器
*/
public class TokenFilter extends UserFilter {
@Override
public void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
System.out.println("token过滤器已处理");
// ...你想要的业务
chain.doFilter(request, response);
}
}
/**
* @Author: jiarupc
* @Date: 2020/08/26
* @Description: shiro 配置类:用于完成 shiro 配置
*/
@Configuration
public class ShiroConfiguration {
/***************** shiro 基础配置 *****************/
/**
* 配置shiro过滤器
* @param manager 已在 Spring 容器中注册的“安全管理器”
* @return
*/
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilter(@Qualifier("securityManager") SecurityManager manager) {
/** 基础配置 **/
// 定义shiro过滤器工厂bean
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
/** 自定义过滤器 **/
Map<String, Filter> filterMap = Maps.newLinkedHashMap();
// 登录token过滤器
filterMap.put("token", new TokenFilter());
bean.setFilters(filterMap);
return bean;
}
}
整个流程非常简单,我们只要继承 UserFilter,在上面写自己的业务,然后加一行配置就行了。拓展性这么好,Shiro 是怎么做到的呢?
解析 Shiro 源码
Shiro 的过滤器用到了责任链模式。追踪 Shiro 的源码,我们可以发现一个类 ProxiedFilterChain。我们看下里面的关键代码:
/**
* FilterChain 的代理类,负责执行 FilterChain
*/
public class ProxiedFilterChain implements FilterChain {
// 初始化时,会从外部传入一个 FilterChain
private FilterChain orig;
// 需要执行的 Filter链
private List<Filter> filters;
// 当前执行到哪个 Filter
private int index = 0;
public ProxiedFilterChain(FilterChain orig, List<Filter> filters) {
// ProxiedFilterChain 只是一个代理类,必须传入 FilterChain 才能执行
if (orig == null) {
throw new NullPointerException("original FilterChain cannot be null.");
}
this.orig = orig;
this.filters = filters;
this.index = 0;
}
public void doFilter(ServletRequest request, ServletResponse response) {
// filter 链为空时,直接调用原来 FilterChain 的方法
if (this.filters == null || this.filters.size() == this.index) {
this.orig.doFilter(request, response);
} else {
this.filters.get(this.index++).doFilter(request, response, this);
}
}
}
我们可以看到,Shiro 拓展了责任链模式,增加了一个代理类。
作为一个权限框架,Shiro 要执行的责任链不止一条。如果每条责任链都写自己执行的代码,那肯定造成代码冗余。所以,Shiro 就把重复代码抽出来,成为一个代理类,专门来执行责任链的业务。
那么,Shiro 是怎么实现责任链模式的呢?
Shiro 的代码非常巧妙,我们把代理类和过滤器合起来看,会发现:它们实现了一个递归调用
。先来看代理类 ProxiedFilterChain.doFilter() 方法。我重新贴上关键代码:
/**
* FilterChain 的代理类,负责执行 FilterChain
*/
public class ProxiedFilterChain implements FilterChain {
// 初始化时,会从外部传入一个 FilterChain
private FilterChain orig;
// 需要执行的 Filter链
private List<Filter> filters;
// 当前执行到哪个 Filter
private int index = 0;
public void doFilter(ServletRequest request, ServletResponse response) {
// filter 链为空时,直接调用原来 FilterChain 的方法
if (this.filters == null || this.filters.size() == this.index) {
this.orig.doFilter(request, response);
}
// 调用 filter.doFilter()
else {
this.filters.get(this.index++).doFilter(request, response, this);
}
}
}
在 ProxiedFilterChain 的第 19 行代码,它为了让 Filter 链顺利执行,做了两个操作。首先, this.filters.get(index++),这段代码的意思是:先获取计数器的下标,通过下标拿到对应的 Filter,再把计数器 + 1。最后,执行过滤器 Filter.doFilter() 方法的。
再来看看 TokenFilter,在实现了业务代码后,这里的第 10 行代码,会重新调用 ProxiedFilterChain.doFilter 方法。
public class TokenFilter extends UserFilter {
// doFilterInternal 等于 doFilter,shiro 对 doFilter 做了特殊处理,不允许修改
@Override
public void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
System.out.println("token过滤器已处理");
// ...你想要的业务
// 这行代码会调用 ProxiedFilterChain.doFilter(),找到实现递归调用
chain.doFilter(request, response);
}
}
这样一来,就实现了递归调用
。如果你还是觉得很难理解,可以再看看下面这副流程图:
写在最后
责任链模式常用在框架开发上,来实现框架的过滤器、拦截器功能。使用者不需要修改框架源码,就能添加新的过滤拦截功能。在 Shiro 的过滤器中,就用到了责任链模式。
当然,我们在日常工作中,很少能碰上 Shiro 那样的大项目,但你总得做好准备。说不定,在某一天,机会就来的呢?
网友评论