过滤器
概述
在 Java 中的最小程序单元是类,程序中的过滤器就是一个特殊的类。类似 Servlet 就是 Web 的一个组件。Filter 也是一个组件。
作用
过滤器处于客户端和服务端之间,可以对所有的请求或者响应做拦截操作。
- 以常规的方式调用资源(Servlet / JSP);
- 利用修改过的请求调用信息;
- 调用资源之后,但在响应到客户端之前,对响应做出修改;
- 阻止当前资源调用,转而调用其他资源。
在开发中的应用:
- 可以对请求中的字符做编码操作;
- 登录验证过滤器
- 敏感字过滤(非法文字过滤)
- 做 MVC 框架中的前端控制器(处理所有请求共同的操作,再分发)
开发中两个常用的思想:
- DRY原则:Don’t Repeat Yourself.
开发中拒绝任何代码重复(重复代码不利于维护,以及性能较差) - 责任分离原则: 每段代码只做自己最擅长的事情
Filter:
1.
当服务器启动时,便会执行 Filter 接口中的 init 和构造器方法:
public FilterDemo() {
System.out.println("构造器");
}
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("init");
}
FilterChain(过滤器链): 多个过滤器按照一定的顺序排列起来。
拦截器栈
当有多个过滤器时,按照 filter-mapping 的顺序进行拦截:
<filter>
<filter-name>FilterDemo</filter-name>
<filter-class>com.cherry._01_hello.FilterDemo</filter-class>
</filter>
<filter>
<filter-name>FilterDemo2</filter-name>
<filter-class>com.cherry._01_hello.FilterDemo2</filter-class>
</filter>
<filter>
<filter-name>FilterDemo3</filter-name>
<filter-class>com.cherry._01_hello.FilterDemo3</filter-class>
</filter>
<filter-mapping>
<filter-name>FilterDemo2</filter-name>
<url-pattern>/test.jsp</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>FilterDemo3</filter-name>
<url-pattern>/test.jsp</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>FilterDemo</filter-name>
<url-pattern>/test.jsp</url-pattern>
</filter-mapping>
输出结果为:
FilterDemo2...before
FilterDemo3...before
FilterDemo1...before
FilterDemo1...after
FilterDemo3...after
FilterDemo2...after
<filter-mapping>
<filter-name>FilterDemo</filter-name>
<url-pattern> /* </url-pattern>
<dispatcher>REQUEST</dispatcher>
</filter-mapping>
这里的 dispatcher 设置 REQUEST 会对于重定向的请求拦截。
<dispatcher>FORWARD</dispatcher>
这里的 dispatcher 设置 FORWARD 会对请求转发进行拦截。
<dispatcher>INCLUDE</dispatcher>
对请求包含做拦截
<dispatcher>ERROR</dispatcher>
跳转到错误页面做拦截
自己写一个404页面:
<error-page>
<error-code>404</error-code>
<location>/404.jsp</location>
</error-page>
<%@ page language="java" contentType="text/html; charset=UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
404,请求的资源找不到
</body>
</html>
可以看到对错误页面的跳转也拦截了一次。
请求编码过滤器
<filter>
<filter-name>CharacterEncodingFilter</filter-name>
<filter-class>com.cherry._02_characterencoding.CharacterEncodingFilter</filter-class>
<!-- 设置编码 -->
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<!-- 是否强制使用编码 -->
<init-param>
<param-name>force</param-name>
<param-value>true</param-value>
</init-param>
</filter>
CharacterEncodingFilter.java:
package com.cherry._02_characterencoding;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
//字符编码过滤器
public class CharacterEncodingFilter implements Filter {
private String encoding;
private boolean forceEncoding = false;
public void init(FilterConfig config) throws ServletException {
this.encoding = config.getInitParameter("encoding");
forceEncoding = Boolean.valueOf(config.getInitParameter("force"));
}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
//类型转换
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
//设置编码
//1.应用中没有编码并且自己设置了编码
//2.应用中已经存在编码,但是依然使用自己设置的编码
if (hasLength(encoding) && (req.getCharacterEncoding() == null || forceEncoding)) {
req.setCharacterEncoding(encoding);
}
chain.doFilter(req, resp);
}
private boolean hasLength(String str) {
return str != null && "".equals(str.trim());
}
}
我们需要做判断:
如果自己设置了编码并且没有默认编码,则使用自己设定的编码;
如果自己设定了编码并且有默认编码,但是需要强制使用自己设定的编码,则使用自己设定的编码。
我们在配置文件中再加入一个参数 force,用来设定是否要强制使用自己设定的编码。
登录检查过滤器
判断登录与否,需要在每一个页面都要添加类似于下面的代码:
<%
Object obj = session.getAttribute("USER_IN_SESSION");
if (obj == null) {
response.sendRedirect("/login.jsp");
}
%>
很是麻烦。通过过滤器可以很容易的解决这一问题。
CheckLoginFilter.java:
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
//登录检查过滤器
public class CheckLoginFilter implements Filter {
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
Object obj = req.getSession().getAttribute("USER_IN_SESSION");
//若没有登录
if (obj == null) {
resp.sendRedirect("/login.jsp");
return;
}
chain.doFilter(req, resp);
}
}
但是这样浏览器会因为重定向次数过多(循环重定向)而崩溃:
很好理解,当请求的资源里没有登录信息时,会被过滤拦截,然后请求登录界面,然而登录界面是在请求 Servlet 时才有登录信息,而本身的登录界面一开始是没有登录信息的,因此又会被拦截,从而接着请求登录界面。。。
解决的话只要加上一个判断,判断当前请求的资源是否是登录界面:
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.mysql.cj.result.StringValueFactory;
//登录检查过滤器
public class CheckLoginFilter implements Filter {
private static int cnt = 0;
private String[] unCheckURIs = { "/login.jsp", "/login" };
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
String uri = req.getRequestURI();
System.out.println("当前正在过滤的资源是:" + uri + " " + cnt++);
if (!Arrays.asList(unCheckURIs).contains(uri)) {
Object obj = req.getSession().getAttribute("USER_IN_SESSION");
//若没有登录
if (obj == null) {
resp.sendRedirect("/login.jsp");
return;
}
}
chain.doFilter(req, resp);
}
}
但是请求的资源不一定都是 /login.jsp 或者 /login,其中一个办法就是将不需要过滤的资源放在 web.xml 配置文件中,也可以新建一个 unCheckUris.xml 文件:
<init-param>
<param-name>unCheckUris</param-name>
<param-value>unCheckUris.xml</param-value>
</init-param>
<unCheckUris>
<uri>/login.jsp</uri>
<uri>/login</uri>
...
</unCheckUris>
但是在网页中,图片资源等是不需要登录就可以访问,这样做依然很麻烦,我们可以转变思维,将需要过滤的资源放在一起,这样就方便多了。
敏感字过滤器
FilterUtil.java:
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
/**
* 敏感字过滤工具类
*/
public class FilterUtil {
private static List<String> stopWords = new ArrayList<>();
static {
Scanner sc = new Scanner(
Thread.currentThread().getContextClassLoader().getResourceAsStream("stopwords.txt"),
"UTF-8");
while (sc.hasNextLine()) {
String line = sc.nextLine();
if (CommonUtil.hasLength(line)) {
stopWords.add(line);
}
}
sc.close();
}
/**
* 执行过滤的方法
* @param msg
* @return
*/
public static String filter(String msg) {
for (String str : stopWords) {
if (msg.indexOf(str) >= 0) {
msg = msg.replaceAll(str, buildMask(str, "*"));
}
}
return msg;
}
private static String buildMask(String s, String mask) {
StringBuilder sBuilder = new StringBuilder();
for (int i = 0; i < s.length(); i++) {
sBuilder.append(mask);
}
return sBuilder.toString();
}
public static void main(String[] args) {
System.out.println(stopWords);
System.out.println(filter("nmsl and nmslwccnm"));
}
}
结果如下:
我们在请求是通过一个过滤器将敏感词汇变成 **
,然后在 Servlet 中通过 req.getParameter 方法获得参数,但是存在一个问题,过滤后的内容如何传给 Servlet 呢?如果通过 req.setAttribute 的方法,就修改了本身获取参数的机制,不太合理。因此我们要重写实现了 HttpServletRequest 接口的实现类 HttpServletRequestWrapper 的父类 ServletRequestWrapper 中的 getParameter 方法,使得其实现过滤器的功能:
MessageRequestWrapper.java:
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import com.cherry.util.FilterUtil;
//敏感字过滤请求包装类
public class MessageRequestWrapper extends HttpServletRequestWrapper {
public MessageRequestWrapper(HttpServletRequest request) {
super(request);
}
/**
* 覆盖 getParameter, 使之支持敏感字过滤
*/
public String getParameter(String name) {
if ("title".equals(name) || "content".equals(name)) {
//返回过滤之后的 title和 content
return FilterUtil.filter(super.getParameter(name));
}
return super.getParameter(name);
}
}
然后新建一个过滤器 MessageFilter:
MessageFilter.java:
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.cherry._04_stopwords.request.MessageRequestWrapper;
/**
* 敏感字监听器
* @author Bryan
*
*/
@WebFilter("/*")
public class MessageFilter implements Filter {
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
//把不具有处理敏感字的请求对象换成可以处理敏感字的请求对象
HttpServletRequest req = (HttpServletRequest) request;
//装饰模式,将具有敏感字过滤功能包装在请求中
HttpServletRequest requestWrapper = new MessageRequestWrapper(req);
chain.doFilter(requestWrapper, response);
}
}
这里实际用到了一种包装设计模式,也叫装饰者模式,将一定功能包装在对象中,这里是将具有敏感字过滤功能的请求包装在原先的请求中。
监听器
Web 中的监听器,主要用于监听作用域对象的创建,监听作用域对象属性的添加、删除、替换。
- 监听作用域对象的创建和销毁
ServletRequestListener: 监听请求对象的创建和销毁
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package javax.servlet;
import java.util.EventListener;
/**
* A ServletRequestListener can be implemented by the developer
* interested in being notified of requests coming in and out of
* scope in a web component. A request is defined as coming into
* scope when it is about to enter the first servlet or filter
* in each web application, as going out of scope when it exits
* the last servlet or the first filter in the chain.
*
* @since Servlet 2.4
*/
public interface ServletRequestListener extends EventListener {
/**
* The request is about to go out of scope of the web application.
* The default implementation is a NO-OP.
* @param sre Information about the request
*/
public default void requestDestroyed (ServletRequestEvent sre) {
}
/**
* The request is about to come into scope of the web application.
* The default implementation is a NO-OP.
* @param sre Information about the request
*/
public default void requestInitialized (ServletRequestEvent sre) {
}
}
HttpSessionListener: 监听会话对象的创建和销毁
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package javax.servlet.http;
import java.util.EventListener;
/**
* Implementations of this interface are notified of changes to the list of
* active sessions in a web application. To receive notification events, the
* implementation class must be configured in the deployment descriptor for the
* web application.
*
* @see HttpSessionEvent
* @since v 2.3
*/
public interface HttpSessionListener extends EventListener {
/**
* Notification that a session was created.
* The default implementation is a NO-OP.
*
* @param se
* the notification event
*/
public default void sessionCreated(HttpSessionEvent se) {
}
/**
* Notification that a session is about to be invalidated.
* The default implementation is a NO-OP.
*
* @param se
* the notification event
*/
public default void sessionDestroyed(HttpSessionEvent se) {
}
}
ServletContextListener: 监听上下文的创建和销毁 (Spring中会用到)
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package javax.servlet;
import java.util.EventListener;
/**
* Implementations of this interface receive notifications about changes to the
* servlet context of the web application they are part of. To receive
* notification events, the implementation class must be configured in the
* deployment descriptor for the web application.
*
* @see ServletContextEvent
* @since v 2.3
*/
public interface ServletContextListener extends EventListener {
/**
** Notification that the web application initialization process is starting.
* All ServletContextListeners are notified of context initialization before
* any filter or servlet in the web application is initialized.
* The default implementation is a NO-OP.
* @param sce Information about the ServletContext that was initialized
*/
public default void contextInitialized(ServletContextEvent sce) {
}
/**
** Notification that the servlet context is about to be shut down. All
* servlets and filters have been destroy()ed before any
* ServletContextListeners are notified of context destruction.
* The default implementation is a NO-OP.
* @param sce Information about the ServletContext that was destroyed
*/
public default void contextDestroyed(ServletContextEvent sce) {
}
}
然后在 web.xml 中配置(一般在 web.xml 中先配 Listener,然后配 Filter,最后配 Servlet)
<listener>
<listener-class>com.cherry._05_listener.ContextLoaderListener</listener-class>
</listener>
这里没有 mapping 标签的原因是因为:监听器要监听谁,就实现哪个接口就行了。
更简单的是知己打一个标签即可:
@WebListener
Web 中的监听器组件没有初始化参数配置,如果要解决监听器中的编码,只能使用全局初始化参数
- 监听作用域对象的属性的添加、删除、替换
HttpSessionAttributeListener: 监听 session 作用域中属性的添加、删除和替换
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package javax.servlet.http;
import java.util.EventListener;
/**
* This listener interface can be implemented in order to get notifications of
* changes to the attribute lists of sessions within this web application.
*
* @since v 2.3
*/
public interface HttpSessionAttributeListener extends EventListener {
/**
* Notification that an attribute has been added to a session. Called after
* the attribute is added.
* The default implementation is a NO-OP.
*
* @param se Information about the added attribute
*/
public default void attributeAdded(HttpSessionBindingEvent se) {
}
/**
* Notification that an attribute has been removed from a session. Called
* after the attribute is removed.
* The default implementation is a NO-OP.
*
* @param se Information about the removed attribute
*/
public default void attributeRemoved(HttpSessionBindingEvent se) {
}
/**
* Notification that an attribute has been replaced in a session. Called
* after the attribute is replaced.
* The default implementation is a NO-OP.
*
* @param se Information about the replaced attribute
*/
public default void attributeReplaced(HttpSessionBindingEvent se) {
}
}
ServletRequestAttributeListener: 监听 request 作用域中属性的添加、删除和替换
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package javax.servlet;
import java.util.EventListener;
/**
* A ServletRequestAttributeListener can be implemented by the
* developer interested in being notified of request attribute
* changes. Notifications will be generated while the request
* is within the scope of the web application in which the listener
* is registered. A request is defined as coming into scope when
* it is about to enter the first servlet or filter in each web
* application, as going out of scope when it exits the last servlet
* or the first filter in the chain.
*
* @since Servlet 2.4
*/
public interface ServletRequestAttributeListener extends EventListener {
/**
* Notification that a new attribute was added to the
* servlet request. Called after the attribute is added.
* The default implementation is a NO-OP.
* @param srae Information about the new request attribute
*/
public default void attributeAdded(ServletRequestAttributeEvent srae) {
}
/**
* Notification that an existing attribute has been removed from the
* servlet request. Called after the attribute is removed.
* The default implementation is a NO-OP.
* @param srae Information about the removed request attribute
*/
public default void attributeRemoved(ServletRequestAttributeEvent srae) {
}
/**
* Notification that an attribute was replaced on the
* servlet request. Called after the attribute is replaced.
* The default implementation is a NO-OP.
* @param srae Information about the replaced request attribute
*/
public default void attributeReplaced(ServletRequestAttributeEvent srae) {
}
}
ServletContextAttributeListener: 监听 application 作用域中属性的添加、删除和替换
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package javax.servlet;
import java.util.EventListener;
/**
* Implementations of this interface receive notifications of changes to the
* attribute list on the servlet context of a web application. To receive
* notification events, the implementation class must be configured in the
* deployment descriptor for the web application.
*
* @see ServletContextAttributeEvent
* @since v 2.3
*/
public interface ServletContextAttributeListener extends EventListener {
/**
* Notification that a new attribute was added to the servlet context.
* Called after the attribute is added.
* The default implementation is a NO-OP.
* @param scae Information about the new attribute
*/
public default void attributeAdded(ServletContextAttributeEvent scae) {
}
/**
* Notification that an existing attribute has been removed from the servlet
* context. Called after the attribute is removed.
* The default implementation is a NO-OP.
* @param scae Information about the removed attribute
*/
public default void attributeRemoved(ServletContextAttributeEvent scae) {
}
/**
* Notification that an attribute on the servlet context has been replaced.
* Called after the attribute is replaced.
* The default implementation is a NO-OP.
* @param scae Information about the replaced attribute
*/
public default void attributeReplaced(ServletContextAttributeEvent scae) {
}
}
HttpSessionAttributeListenerDemo:
@WebListener
public class HttpSessionAttributeListenerDemo implements HttpSessionAttributeListener {
public void attributeAdded(HttpSessionBindingEvent se) {
System.out.println("属性添加:" + se.getName() + "-" + se.getValue());
}
public void attributeRemoved(HttpSessionBindingEvent se) {
System.out.println("属性删除:" + se.getName() + "-" + se.getValue());
}
public void attributeReplaced(HttpSessionBindingEvent se) {
System.out.println("属性替换:" + se.getName() + "-" + se.getValue() + ","
+ se.getSession().getAttribute(se.getName()));
}
}
<%@ page language="java" contentType="text/html; charset=UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<%
session.setAttribute("name", "NAME");
session.setAttribute("age", "AGE");
session.removeAttribute("age");
session.setAttribute("name", "My Name");
%>
<body>
Hello World!
</body>
</html>
值得注意的是,se.getValue() 得到的是之前的属性值,如果要得到修改过后的属性值,要重新获得当前 Session: se.getSession().getAttribute(se.getName())
监听器可以用在在线人数统计,当有人登录时,会监测到 session 会话建立。