详细介绍了Java Web Servlet中的Filter过滤器的原理以及常见用法。
过滤器属于Servlet规范,从2.3版本就开始有了。主要用于对到资源的请求或来自资源的响应执行过滤、筛选操作。
当存在过滤器的时候,对于来自客户端的请求来说,请求必须先经过滤器,放行之后,才能到达Web资源;对于返回的响应来说,响应同样会经过滤器,才能到达Web服务器,进而响应给客户端
过滤器可以做很多事情,常见的包括:
过滤器在Java中对应着javax.servlet.Filter接口,仅此而已,实现了Filter接口的类就可以被称作过滤器,就是这么简单。
Filter接口中有三个抽象方法,其中init和destroy方法作为过滤器的申请周期方法!
public interface Filter {
default public void init(FilterConfig filterConfig) throws ServletException {
}
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain)
throws IOException, ServletException;
default public void destroy() {
}
}
诞生
:过滤器的实例是在web应用被加载时就完成的实例化,并调用init方法初始化的。servlet 容器在实例化Filter后只调用init
方法一次。在要求过滤器执行任何过滤工作之前,init 方法必须成功完成。区别于Servlet,过滤器会全部立即初始化。存活
:和应用的生命周期一致的。在内存中是单例的。针对拦截范围内的资源,每次访问都会调用void doFIlter(request,response.chain)进行拦截。死亡
:应用被卸载时,Filter将被调用,此时会调用destroy方法,该方法只会被调用一次。 过滤器在 doFilter 方法中执行过滤操作。
doFilter方法中有一个FilterChain 参数对象,该对象由Servlet容器创建并传递给开发人员的。FilterChain表示一个过滤器链,客户端请求的资源在链的末尾。
在当前过滤器中,如果复合过滤规则,那么可以使用 FilterChain 的doFilter方法调用链中的下一个过滤器器,或者如果调用过滤器是链中的最后一个过滤器,则该方法将调用链末尾的资源。
也就是说,一个Web应用中可以有多个过滤器,它们将会按照一定顺序形成一个过滤器链,在链的最末尾就是要访问的资源,当一个请求到来的时候,他必须通过所有的过滤器,才能访问到真正的资源。
开发一个过滤器很简单,只需要实现Filter接口,实现doFilter方法。
public class FirstFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) {
System.out.println("init");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
System.out.println("放行前");
HttpServletRequest httpServletRequest= (HttpServletRequest) request;
//获取当前请求的URL
System.out.println(httpServletRequest.getRequestURL());
//放行,调用下一个过滤器或者访问资源
chain.doFilter(request, response);
System.out.println("放行后");
}
@Override
public void destroy() {
System.out.println("destroy");
}
}
随后我们还需要部署这个filter。通常情况下,Filter过滤器在 Web 应用程序的部署描述符中配置,也就是web.xml文件,这类似于Servlet。
首先是定义一个Filter:
<filter>
<filter-name>FirstFilterfilter-name>
<filter-class>com.example.filter.FirstFilterfilter-class>
<init-param>
<param-name>aaaparam-name>
<param-value>bbbparam-value>
init-param>
<init-param>
<param-name>cccparam-name>
<param-value>dddparam-value>
init-param>
filter>
一个
标签表示定义一个过滤器,
表示当前过滤器的name,
表示当前过滤器的类全路径名,
表示当前过滤器的初始化参数,可以通过在init方法
的参数FilterConfig
对象获取这些参数。
随后我们还需要定义这个过滤器可以作用于哪些资源或者哪些Servlet!
<filter-mapping>
<filter-name>FirstFilterfilter-name>
<url-pattern>/aa/*url-pattern>
filter-mapping>
<filter-mapping>
<filter-name>FirstFilterfilter-name>
<servlet-name>Servlet2servlet-name>
filter-mapping>
通过多个
标签可以为一个过滤器配置多个过滤映射,当然也可以将多个映射一个到一个
标签中
指定某个过滤器的名字
指定过滤器所拦截的资源路径URL,“/”表示所有的Web资源都需要途径该过滤器,“.xxx”表示拦截访问xxx后缀的资源的请求。
指定过滤器所拦截的某个Servlet的名字。
指定过滤器在拦截时所应用的调度模式,一共有五个可选配置:FORWARD, REQUEST, INCLUDE, ASYNC, ERROR。
FORWARD
:如果目标资源是通过RequestDispatcher的forward()方法访问的,那么该过滤器将被调用,除此之外,该过滤器不会被调用。REQUEST
:当用户直接通过普通路径访问资源时,Web容器将会调用过滤器。如果目标资源是通过RequestDispatcher的include()或forward()方法访问时,那么该过滤器就不会被调用。这是默认的模式。INCLUDE
:如果目标资源是通过RequestDispatcher的include()方法访问的,那么该过滤器将被调用,除此之外,该过滤器不会被调用。ERROR
:如果目标资源是通过声明式的异常处理机制调用的,那么该过滤器将被调用。除此之外,过滤器不会被调用。SYNC
:意味着过滤器将在从异步上下文AsyncContext的调用下应用。在Servlet 3.0以及之后,支持在Filter实现类上直接使用@WebFilter注解的方式配置过滤器,降低了配置文件的复杂度。例如:
@WebFilter(urlPatterns = "/aa/*",servletNames = "Servlet2")
下面简单测试,如下有三个Servlet:
@WebServlet(name = "Servlet1", value = "/aa/Servlet1")
public class Servlet1 extends HttpServlet {
@Override
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
System.out.println("请求到达");
response.setContentType("text/html;charset=utf-8");
PrintWriter out = response.getWriter();
out.println("Servlet1");
}
}
@WebServlet(name = "Servlet2", value = "/bb/Servlet2")
public class Servlet2 extends HttpServlet {
@Override
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
System.out.println("请求到达");
response.setContentType("text/html;charset=utf-8");
PrintWriter out = response.getWriter();
out.println("Servlet2");
}
}
@WebServlet(name = "Servlet3", value = "/bb/Servlet3")
public class Servlet3 extends HttpServlet {
@Override
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
System.out.println("请求到达");
response.setContentType("text/html;charset=utf-8");
PrintWriter out = response.getWriter();
out.println("Servlet3");
}
}
如果我们采用上面的过滤器设置,那么在访问Servlet1和Servlet2的时候,将会经过过滤器,而访问Servlet3的时候则不会经过过滤器,控制台输出为:
如果我们注释掉chain.doFilter方法,即不放行,那么访问Servlet1和Servlet2的时候,页面上不会有任何输出,也不会输出控制台也不会输出“请求到达”字符串,因为请求被拦截了,而访问Servlet3的时候则可以正常访问:
上面的简单测试,我们可能会发现,在chain.doFilter之前的逻辑在请求到达指定的Servlet之前执行,chain.doFilter之后的逻辑则是在请求到达指定的Servlet并离开之后执行。
过滤器链的完整流程顺序是这样的:客户端发送http请求到Web服务器上,Web服务器对该请求URL找到对应负责的过滤器形成过滤器链,接着从第一个过滤器开始进行过滤操作,也就是调用Filter.doFilter方法,这个方法的逻辑是开发者编写的,当当前请求满足当前过滤器的要求或者是过滤操作完毕之后,应在调用chain.doFilter方法进行放行,该方法又会调用链中的下一个过滤器的doFilter方法继续过滤,如果当前过滤器是过滤器链的最后一个过滤器,那么chain.doFilter方法将会执行资源访问操作,访问完毕之后,将会依照最开始过滤器的调用顺序倒序的返回,接着执行chain.doFilter方法后面的代码。最终将响应结果交给Web服务器,Web服务器再将响应返回给客户端。
当某个请求对应存在多个过滤器时,过滤器之间的执行顺序是通过在web.xml文件中某个Filter的首个
定义的先后顺序决定的。
如下,两个Filter:
public class Filter1 implements Filter {
@Override
public void init(FilterConfig filterConfig) {
System.out.println("Filter1-init");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
System.out.println("-----Filter1放行前-----");
HttpServletRequest httpServletRequest= (HttpServletRequest) request;
//获取当前请求的URL
System.out.println(httpServletRequest.getRequestURL());
//放行,调用下一个过滤器或者访问资源
chain.doFilter(request, response);
System.out.println("-----Filter1放行后-----");
}
@Override
public void destroy() {
System.out.println("destroy");
}
}
public class Filter2 implements Filter {
@Override
public void init(FilterConfig filterConfig) {
System.out.println("Filter2-init");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
System.out.println("-----Filter2放行前-----");
HttpServletRequest httpServletRequest= (HttpServletRequest) request;
//获取当前请求的URL
System.out.println(httpServletRequest.getRequestURL());
//放行,调用下一个过滤器或者访问资源
chain.doFilter(request, response);
System.out.println("-----Filter2放行后-----");
}
@Override
public void destroy() {
System.out.println("destroy");
}
}
以下是映射配置:
<filter>
<filter-name>Filter1filter-name>
<filter-class>com.example.filter.Filter1filter-class>
filter>
<filter>
<filter-name>Filter2filter-name>
<filter-class>com.example.filter.Filter2filter-class>
filter>
<filter-mapping>
<filter-name>Filter2filter-name>
<servlet-name>Servlet2servlet-name>
filter-mapping>
<filter-mapping>
<filter-name>Filter1filter-name>
<url-pattern>/aa/*url-pattern>
<servlet-name>Servlet2servlet-name>
filter-mapping>
<filter-mapping>
<filter-name>Filter2filter-name>
<url-pattern>/aa/*url-pattern>
filter-mapping>
可以看到Filter2的mapping在前,因此Filter2将先被执行,但是在资源访问完毕返回时,则是倒序执行!
我们访问Servlet1和Servlet2,将会得到如下结果:
如果是通过注解的方式配置,就比较urlPatterns的字符串优先级。
Filter的简单应用包括:
chain.doFilter(request,response)
方法,即是否让目标资源能够访问。脏话过滤器,将参数中的脏话替换为**。
/**
* 脏话过滤器,拦截所有访问路径
*
* @author lx
*/
@WebFilter("/*")
public class DirtyWordsFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) {
System.out.println("DirtyWordsFilter-init");
}
@Override
public void doFilter(ServletRequest req, ServletResponse resp,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest request;
HttpServletResponse response;
try {
request = (HttpServletRequest) req;
response = (HttpServletResponse) resp;
} catch (Exception e) {
throw new RuntimeException("non-http request or response");
}
//传递一个装饰类,该类的getParameter能够过滤某些脏话
DWHttpServletRequest dwrequest = new DWHttpServletRequest(request);
chain.doFilter(dwrequest, response);
}
@Override
public void destroy() {
System.out.println("destroy");
}
}
/**
* 装饰类,接收一个request对象
*/
class DWHttpServletRequest extends HttpServletRequestWrapper {
/**
* 脏话词典
*/
private String[] strs = {
"坤坤", "凡凡", "禽兽", "畜生", "傻B"};
public DWHttpServletRequest(HttpServletRequest request) {
super(request);
}
/**
* 增强了getParameter方法
*
* 将参数中的脏话替换为**才返回
*/
@Override
public String getParameter(String name) {
String value = super.getParameter(name);
if (value == null) {
return value;
}
for (String s : strs) {
//将脏话替换为**才返回
value = value.replace(s, "**");
}
return value;
}
}
测试Servlet:
@WebServlet("/DirtyWordsServlet")
public class DirtyWordsServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
String name = req.getParameter("name");
resp.setContentType("text/html;charset=utf-8");
PrintWriter out = resp.getWriter();
out.println(""
+ name + "");
}
}
尝试请求http://localhost:8081/filter/DirtyWordsServlet?name=坤坤,返回结果如下:
我们前面说过,tomcat8以下的版本默认对于URL采用iso-8859-1解码,那么get请求参数具有中文等特殊字符时,获取参数值的时候需要进行转码,并且在对于post请求的参数也需要通过setCharacterEncoding设置编码,非常麻烦。
现在有了Filter过滤器,我们可以使用一个Filter解决全部的get请求的乱码问题,这里涉及到动态代理的应用(tomcat8及其以上不适用,因为tomcat8默认对于URL采用utf-8解码,可以直接获取正常的参数值)。
同时,还能统一设置Post请求编码,以及响应编码,有了这个过滤器,就不必在各个Servlet中设置请求编码或者响应编码了。
Spring MVC中的CharacterEncodingFilter就是一个编码过滤器的实现!
@WebFilter("/*")
public class EncodingFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) {
System.out.println("EncodingFilter-init");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
//将request和response强转成http协议的
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
/*
* 通用编码设置
*/
//解决post请求提交参数的乱码问题
req.setCharacterEncoding("UTF-8");
//统一设置响应编码
resp.setContentType("text/html;charset=UTF-8");
//使用JDK的动态代理,动态的增强req对象的getParameter(name)方法
//解决tomcat8一下的版本的get请求url乱码的问题(tomcat8之后的版本不需要了)
HttpServletRequest myReq = (HttpServletRequest) Proxy.newProxyInstance(HttpServletRequest.class.getClassLoader(), new Class[]{
HttpServletRequest.class}, (proxy, method, args) -> {
Object obj;
//如果是getParameter方法被调用
if ("getParameter".equalsIgnoreCase(method.getName())) {
//获取本次的请求方法
String md = req.getMethod();
//解决get请求提交参数的乱码问题,动态增强
if ("get".equalsIgnoreCase(md)) {
//调用目标对象的getParameter方法
String v = (String) method.invoke(req, args);
//对获取到的结果进行转码(tomcat8之后的版本不需要了)
return new String(v.getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
}
//其他请求
obj = method.invoke(req, args);
}
//如果是其他方法被调用,那么直接通过目标对象调用
else {
obj = method.invoke(req, args);
}
return obj;
});
//将代理对象放行
chain.doFilter(myReq, response);
}
@Override
public void destroy() {
System.out.println("destroy");
}
}
IP工具类:
/**
* @author lx
*/
public class IpUtil {
/**
* 获取用户真实IP地址
*
* @param request 请求
* @return ip
*/
public static String getIpAddr(HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
System.out.println("x-forwarded-for ip: " + ip);
if (ip != null && ip.length() != 0 && !"unknown".equalsIgnoreCase(ip)) {
// 多次反向代理后会有多个ip值,第一个ip才是真实ip
if (ip.contains(",")) {
ip = ip.split(",")[0];
}
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
System.out.println("Proxy-Client-IP ip: " + ip);
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
System.out.println("WL-Proxy-Client-IP ip: " + ip);
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
System.out.println("HTTP_CLIENT_IP ip: " + ip);
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
System.out.println("HTTP_X_FORWARDED_FOR ip: " + ip);
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Real-IP");
System.out.println("X-Real-IP ip: " + ip);
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
System.out.println("getRemoteAddr ip: " + ip);
}
System.out.println("获取客户端ip: " + ip);
return ip;
}
}
IP统计过滤器:
/**
* 根据ip统计访问次数
*
* @author lx
*/
@WebFilter("/*")
public class IPCountFilter implements Filter {
private FilterConfig config;
/**
* 在init方法中初始化一个map集合,用于存放ip以及它们的访问次数,将map存入ServletContext域对象中
*
* 这个方法只会调用一次
*/
@Override
public void init(FilterConfig config) {
this.config = config;
//准备一个map空集合:
Map<String, Integer> map = new HashMap<>();
//将map集合存放在ServletContext域对象当中:
ServletContext context = config.getServletContext();
//存进去:
context.setAttribute("map", map);
}
@Override
public void doFilter(ServletRequest req,
ServletResponse res, FilterChain chain)
throws IOException, ServletException {
/*
* 开发步骤:
* 1:获得当前请求的ip地址:
* 2:从一个map集合当中进行查询,
* 如果查询不到,存1进去。
* 如果查询到,将值+1后存进去。
* 3:在页面上展示即可
*/
//强制转换:
HttpServletRequest request;
HttpServletResponse response;
try {
request = (HttpServletRequest) req;
response = (HttpServletResponse) res;
} catch (ClassCastException e) {
throw new ServletException("non-HTTP request or response");
}
//获取ip地址
String ip = request.getRemoteAddr();
//从context当中获得map集合:
ServletContext context = config.getServletContext();
Map<String, Integer> map = (Map<String, Integer>) context.getAttribute("map");
//从map集合当中获得ip对应的值,并且记录访问次数
Integer count = map.computeIfAbsent(ip, s -> 0);
//将值存回map集合当中
map.put(ip, ++count);
//map集合存回ServletContext当中:
context.setAttribute("map", map);
//放行
chain.doFilter(request, response);
}
@Override
public void destroy() {
}
}
IPCountServlet:
@WebServlet("/IPCountServlet")
public class IPCountServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html;charset=utf-8");
PrintWriter out = resp.getWriter();
ServletContext servletContext = getServletContext();
//从context当中获得map集合:
Map<String, Integer> map = (Map<String, Integer>) servletContext.getAttribute("map");
for (Map.Entry<String, Integer> stringIntegerEntry : map.entrySet()) {
out.println("IP: " + stringIntegerEntry.getKey() + "的访问次数为: " + stringIntegerEntry.getValue());
}
}
}
注意,如果是localhost,那么可能将获取0:0:0:0:0:0:0:1的IPV6的形式地址,对应ipv4来说相当于127.0.0.1,也就是本机。
假设动态资源访问路径为“/Servlet”和“.JSP”。Servlet、JSP等动态资源不应该被浏览器缓存,因为响应结果随时可能改变。
@WebFilter(urlPatterns = {
"/servlet/*", "*.jsp"})
public class NocacheFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) {
System.out.println("NocacheFilter-init");
}
@Override
public void doFilter(ServletRequest req, ServletResponse resp,
FilterChain chain) throws IOException, ServletException {
//强制转换:
HttpServletRequest request;
HttpServletResponse response;
try {
request = (HttpServletRequest) req;
response = (HttpServletResponse) resp;
} catch (ClassCastException e) {
throw new ServletException("non-HTTP request or response");
}
//设置动态资源的过期时间和浏览器不缓存
response.setHeader("Expires", "-1");
response.setHeader("Cache-Control", "no-cache");
response.setHeader("Pragma", "no-cache");
//放行:
chain.doFilter(request, response);
}
@Override
public void destroy() {
System.out.println("destroy");
}
}
@WebFilter("/*")
public class HTMLFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) {
System.out.println("HTMLFilter-init");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
//增强对象
HTMLHttpServlet htmlHttpServlet = new HTMLHttpServlet((HttpServletRequest) request);
//放行
chain.doFilter(htmlHttpServlet, response);
}
@Override
public void destroy() {
System.out.println("destroy");
}
}
/**
* html标记过滤器,增强类
*/
class HTMLHttpServlet extends HttpServletRequestWrapper {
/**
* Constructs a request object wrapping the given request.
*
* @param request the {@link HttpServletRequest} to be wrapped.
* @throws IllegalArgumentException if the request is null
*/
public HTMLHttpServlet(HttpServletRequest request) {
super(request);
}
/**
* 增强getParameter方法
*/
@Override
public String getParameter(String name) {
String value = super.getParameter(name);
return filter(value);
}
/**
* tomcat 当中webapps/examples项目下的/WEB-INF/classes/utils下的工具类
*
* Filter the specified message string for characters that are sensitive
* in HTML. This avoids potential attacks caused by including JavaScript
* codes in the request URL that is often reported in error messages.
*
* @param message The message string to be filtered
* @return the filtered version of the message
*/
public static String filter(String message) {
if (message == null)
return null;
char content[] = new char[message.length()];
message.getChars(0, message.length(), content, 0);
StringBuilder result = new StringBuilder(content.length + 50);
for (char c : content) {
switch (c) {
case '<':
result.append("<");
break;
case '>':
result.append(">");
break;
case '&':
result.append("&");
break;
case '"':
result.append(""");
break;
default:
result.append(c);
}
}
return result.toString();
}
}
如有需要交流,或者文章有误,请直接留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!