本系列文章旨在记录和总结自己在Java Web开发之路上的知识点、经验、问题和思考,希望能帮助更多( Java )码农和想成为(Java)码农的人。
目录
- 介绍
- 拦截
- 流程
- 职责单一
- 消除重复
- 维度
- 实现一个 Filter
介绍
我们使用 servlet 技术实现了一个简单的租房网平台,最后的总结中提到是否可以将重复的登录验证逻辑使用Filter来实现,答案显然是可以的。
Filter相关的接口可以参考 ,此处不再赘述。
拦截
首先,Filter从名字上翻译过来是:
它的作用实际上就是对经过它的某物执行某些动作,这些动作可以是剔除不需要的,也可以是添加一些需要的,也可以是进行某种变换等等。
从这个解释上说,Filter运用了 拦截 的思维。
拦截的思维还是比较常见的,比如代理、中介、拦截器、过滤器、装饰器等等都带有拦截的成分。
既然是拦截,那必然涉及到 在哪 拦截的问题,对于我们Servlet技术中的Filter,它是在请求到达Servlet的service()方法之前,以及service()方法执行完毕之后。即Filter已经将这两处拦截地方(暂且将前者拦截之后的处理称为Filter的 前置处理 ,将后者拦截之后的处理称为Filter的 后置处理 )合二为一了,所以Servlet API中也就没有所谓的PreFilter或者PostFilter之类的了。
所以,Servlet技术中的Filter包括了两个部分,前置处理和后置处理。它们都存在于Filter.doFilter()方法的方法体之中。当然,这两个部分都是由我们来实现的,有可能都存在,但一般情况之下都至少存在一个部分。如果都不存在,那就没有必要实现该Filter了啊。
一般情况下,前置处理处理的是请求,后置处理处理的是响应。但也并非一定如此,因为Filter.doFilter()方法是由Servlet容器调用的,Servlet容器已经把封装好的请求和响应作为该方法的参数传进来了,尽管此时响应还没有真正的响应(仅封装发送细节),所以你也可以对响应执行某些操作。
既然是拦截,那就必然涉及到 放行 的问题,即执行完Filter的前置处理之后,有时我们需要将请求/响应(Servlet容器传参进来,下同)继续交给Servlet来处理,然后再执行Filter的后置处理;而有时执行完Filter的前置处理之后,可能不希望请求/响应继续交给Servlet来处理,而是直接执行后置处理(如果有的话)。
Servlet API中提供了一个FilterChain接口(Servlet容器将配置的多个Filter生成FilterChain实例,然后也传参给Filter.doFilter()方法)来实现放行的问题。具体来说就是,如果我们在Filter.doFilter()方法体中调用了 FilterChain .doFilter()方法,那么就表示放行;否则就不放行(那如果调用多次会是什么结果,大家可以先想想再测试一下)。
实际上, FilterChain .doFilter()方法的调用也是区分Filter中前置处理和后置处理的标志。当然,现实中往往要复杂得多,比如,仅仅在某种条件下才需要放行,然后还存在一些不放行与放行都需要的处理,那这一部分就既是(不放行时)前置处理,又是(放行时)后置处理。所以,这种区分也是相对的。
综上所述,Filter的工作模式就像下面那样(箭头的方向表示请求/响应的流向):
关于拦截,还有什么样的请求才需要拦截的问题,这就是Filter与请求的映射问题。这个跟Servlet映射差不多,可以通过配置URL Pattern来匹配访问某些资源的请求。
同时,Filter的映射里面还可以配置请求分派的类型,因为请求可以通过普通的、转发、重定向、错误等方式分派给一个Servlet,所以也可以指定只拦截某种分派类型的请求。
关于Filter配置的这些内容,大家可以看看@WebFilter这个注解的源码和javadoc。如果使用部署描述符web.xml来配置,则可以利用Eclipse的自动补全功能,在此就不再赘述了。
流程
做任何事情,都可以也应该按照一定的流程来推进,这就是 流程 的思维。
不管是现实生活中到银行办某件事,到政府部门办某件事,网购,租房,买房,分步走战略,五年计划等等,还是将现实世界抽象到计算机世界的程序,也必然可以也应该按照一定的流程来推进和执行。
软件领域中的流程图、工作流、责任链、流式处理引擎、业务流程、某某链、状态图、活动图等,无一不带有流程的思维,不过细节和侧重点不太一样而已。
流程一般是由若干个步骤/节点组成,它们之间以一定顺序在满足某些条件时推进/执行,推进/执行时有的步骤/节点可能会重复,也可能不重复。
实际上,这有点像数学中的图的概念了。但我还是称之为 流程思维 。
体现在Servlet技术中的就是 FilterChain 这个接口了,它就是为了解决现实中往往需要按照流程来推进的需求。所以,Servlet技术中允许实现并配置多个Filter,形成一个FilterChain来拦截并推进对某个资源的访问流程,比如先要验证登录,再验证操作权限,再处理字符编解码,再统计计数,再解密,再给图片加个水印,再压缩等等。
显然,流程思维有很多好处:
- 容易监控某个业务进行到哪个步骤;
- 灵活易扩展,需要某个步骤就实现并挂载上,不需要就卸载它。
- 代码复用提高,一个Filter可以挂载到多个流程上。
- 等等
所以,进一步的,Servlet技术中的Filter的工作模式就变成这样:
只是多加了一个Filter来表示支持配置多个Filter形成链式的拦截和处理。
整个流程的每个Filter的执行与否,还是以前驱是否调用了 FilterChain .doFilter()方法。
每个Filter的前置处理和后置处理的执行是成对的(如果存在后置处理的话),并且各个Filter之间不会交叉嵌套执行,就好像XML语言或HTML语言中开始标签和结束标签是不会交叉嵌套的。
需要注意的是,一旦流程在某个Filter停止了,即在该Filter的doFilter()方法体中没有调用 FilterChain .doFilter()方法,那么表示该Filter就没有后置处理,接着还会执行上一个Filter的后置处理(如果有的话)。
最后, FilterChain .doFilter()方法并没有各个Filter的执行顺序的体现,实际上,Filter的执行顺序只能在部署描述符web.xml中通过定义的先后顺序来确定,即先定义的先执行。
职责单一
Filter的存在也是职责单一思维的体现。
一开始,或许我们并没有意识到某些地方/操作能形成某个职责,所以将它们都糅合在了一个Servlet中。
慢慢的,随着业务的发展,应用也要不断的演进,我们就会意识到某些地方/操作能形成某个职责,因而将它们抽象出一个独立的对象中。
这或许是那些高级专家与普通程序员的区别,高级专家能预见到这种问题,所以在项目一开始就可以做出好的设计决策。
发现职责的一个常用方法就是,看有没有重复的代码、重复的操作。
消除重复
消除重复 当然是Filter存在的一个理由或目的,甚至是大部分技术存在的理由或目的。
因为Filter可以拦截请求和响应,所以可以用它来实现一些通用的需求/功能,比如在我们的租房网平台中各个Servlet方法中重复的用户登录验证。
同时,一个Filter可以挂载到多个流程中,这里的流程实际上就是指对不同资源的访问流程。
重复,可以分为静态重复和动态重复。
静态重复是指某些静态物件的重复,在我们软件领域最常见的应该就是某些源代码的重复,即相同的代码出现在了多个地方。当然,还有其他方面的重复,比如配置参数、版本库等等。本质上,静态重复是动态重复导致的。
动态重复是指某个行为的重复,当然,这主要是指人的行为,比如,不断拷贝粘贴通用的源代码;不断的修改、测试、部署应用等等。这些行为应该尽量做成软件交给计算机去执行。因为计算机擅长做重复的事情且不易出错;而人只喜欢做分析做决定做判断,重复的行为会导致厌烦、出错(这样看来,那些制造业流水线上的工人们真的是很细心和很有耐性啊)。
这样看来,静态重复本质上是动态重复导致的。
重复当然是很不好的,除了上面提到的,还因为一旦某个重复的地方需要做修改,那么就要改动所有重复的地方,你并不一定能记住所有重复的地方,所以容易出错。当然,你可以使用搜索。
不过,重复也有好的地方。最常见的就是我们的备份,这也算是重复啊。比如应用的备份、数据的备份。
维度
首先,Servlet技术的设计上是规定某一个请求最终有且只能有一个Servlet来处理。
但往往有些需求/功能是 通用的 ,即多个Servlet都要使用到的,最典型的比如每个请求进入Servlet之时以及每个响应发送出去之前都需要记录日志;安全方面的认证授权(租房网平台中的登录验证就是其中一种);访问的计数统计;编码与解码;压缩与解压缩等等。
实际上,这些需求/功能并不是真正的业务,如果说真正的业务是一个维度(可以叫它为 业务维度 ),那么这些通用的需求/功能可以看作是另外一个维度(可以叫它为 通用维度 ),并且这个维度可以看作是垂直于 业务维度 的,像下图一样:
这样看的话,通用维度的功能就好像是一个切面一样跨越了多个业务,这正是面向切面编程( AOP )的思想。
不过,虽然Servlet技术中的Filter谈不上是AOP,却也是运用了维度的思维。这对于我们在判断什么时候该使用Filter还是有一定帮助的。
一句话,维度的思维就是要用不同的维度/角度/视角去看事物。
实现一个Filter
实现一个Filter与实现一个Servlet类似,同样可以使用Eclipse中的New工具,然后再添加自己的功能逻辑代码,或者把不必要的都删除。
下面是我将 中的租房网应用中的一些重复的代码抽出来,交给Filter去执行:
package houserenter.filter;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.Servlet request ;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebFilter("/house.html")
public class MyFirstFilter implements Filter {
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
request.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
String userName = httpServletRequest.getParameter("userName");
if (userName == null || userName.isEmpty()) {
System.out.println("invalid user!");
httpServletResponse.sendRedirect("login.html");
} else {
chain.doFilter(request, response);
}
}
}
主要是将字符编码设置,登录验证的代码移到这里,同时将原来各个Servlet中的相应部分删除(这个比较简单,大家可以自行操作)。
然后,使用@WebFilter注解来配置拦截何种请求,这里配置的是跟HouseServlet一样的,即拦截访问有关房源资源的请求。
大家可以自行运行验证一下,应该没什么问题。
还有,大家也可以再实现并配置若干个拦截相同请求的Filter,这样就形成了FilterChain,然后添加一些打印日志的代码,看看Filter的前置处理和后置处理的执行是怎样的,甚至可以试试连续调用两次FilterChain.doFilter()方法。
总结
主要是通过Servlet中的Filter介绍了几个思维:
- 拦截思维
- 流程思维
- 职责单一思维
- 消除重复思维
- 维度思维
关于Filter的:
- Filter要实现Filter接口(依赖这个接口,即Servlet API会存在 代码入侵 到业务代码中);
- Filter的配置与Servlet类似,可以在部署描述符中web.xml配置,也可以使用@WebFilter注解;
- Filter包含前置处理和后置处理,以FilterChain.doFilter()方法的调用为标志来区分;
- 可以多个Filter形成FilterChain;
- FilterChain.doFilter()方法的调用就是放行的意思,即把请求/响应交给下个Filter,最终会交给Servlet;
- Filter的前置处理和后置处理不会交叉嵌套执行;
- Filter的执行顺序只能通过部署描述符中web.xml中定义的先后顺序来确定