目录
一、过滤器(Filter)
1.Filter过滤器简介
Filter 过滤器它是 JavaWeb 的三大组件之一。三大组件分别是:Servlet 程序、Listener 监听器、Filter 过滤器。Filter 过滤器是 JavaEE 的规范,也就是接口。Filter过滤器主要是用于拦截请求,过滤响应的。结合过滤器可以进行权限检查、编码控制、事务管理等操作。
2.自定义Filter
实现Filter接口
首先自定义过滤器需要实现javax.servlet.Filter接口。实现其中doFilter方法如下(其中init和destroy方法已经有默认实现):
public class HelloFilter implements Filter {
public HelloFilter() {
System.out.println("HelloFilter构造器");
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println(filterConfig.getInitParameter("userName"));
System.out.println("初始化HelloFilter结束");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
System.out.println("前置代码");
//放行(他将调用下一个Filter的doFilter())
chain.doFilter(request, response);
System.out.println("后置代码");
}
@Override
public void destroy() {
System.out.println("销毁HelloFilter");
}
}
- init(FilterConfig)方法:初始化自定义Filter,它与Servlet的init方法作用是一样的,FilterConfig于ServletConfig也类似,利用这两个对象都可以获得容器的环境类ServletContext对象,FilterConfig也可以取到<filter>下配置的<init-param>参数值。注意web工程启动时就会执行其构造方法和init(FilterConfig)方法。(Filter已创建和初始化完成)
- doFilter(ServletRequest,ServletResopnse,FilterChain)方法:在每个用户请求进来时这个方法都会被调用,并且在Servlet的service方法之前被调用。而FilterChain就代表当前的整个请求链,所以通过调用FilterChain对象的doFilter方法可以将请求传递给下一个过滤器。如果想拦截这个请求,可以不调用FilterChain对象的doFilter方法,那么这个请求将被直接返回。这是一种职责链模式。
- destory()方法:当停止 web 工程的时候,就会执行该方法,用于释放当前Filter对象所占用的资源,这个方法将在Filter对象真正被销毁之前被调用。注意,当web容器调用这个方法之后,容器会再调用一次doFilter方法。
web.xml中配置该Filter
<filter>
<filter-name>HelloFilter</filter-name>
<filter-class>com.sgg.filter.test.HelloFilter</filter-class>
<!-- 可以选择配置 -->
<init-param>
<param-name>userName</param-name>
<param-value>root</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>HelloFilter</filter-name>
<!-- 需要拦截过滤的资源路径 -->
<url-pattern>/test.jsp</url-pattern>
</filter-mapping>
在配置filter时,还可以在<filter-mapping>标签内添加<dispatcher>元素,他有四种取值,分别是:REQUEST,FORWARD,INCLUDE和ERROR。我们可以在一个<filter-mapping>中添加多个<dispatcher>元素,如果不添加,其默认为REQUEST。这四种取值的意思如下:
- REQUEST:只要发起的操作是一次HTTP请求,比如请求某个URL、发起了一个GET请求、表单提交方式为POST的POST请求、表单提交方式为GET的GET请求。一次重定向则前后相当于发起了两次请求,这些情况下有几次请求就会走几次指定过滤器。 如果目标资源是通过RequestDispatcher的include()或forward()方法访问时,那么该过滤器就不会被调用。
- FORWARD:如果目标资源是通过请求转发的形式访问时该过滤器才会被调用,(RequestDispatcher的forward()方法,或< jsp:forward page="……" /> 或 page指令的errorPage转发页面.<%@ page errorPage=“……” %>),除此之外,该过滤器不会被调用。
- INCLUDE:如果目标资源是通过RequestDispatcher的include()方法(或 < jsp:include file="/…" />)访问时,那么该过滤器将被调用。除此之外,该过滤器不会被调用。
- ERROR:如果目标资源是通过声明式异常处理机制调用时(web.xml中配置了
<error-page></error-page>
),那么该过滤器将被调用。除此之外,过滤器不会被调用。
3.FilterConfig类
正如上面提到的它与ServletConfig类似,它是每个自定义Filter的配置类。Tomcat 每次创建 Filter 的时候,也会同时创建一个 FilterConfig 类,这里包含了 Filter 配置文件的配置信息。通过该对象可以获取Filter的配置信息:
- 获取 Filter 的名称 filter-name 的内容
- 获取在 Filter 中配置的 init-param 初始化参数值
- 获取 ServletContext 对象
4.FilterChain类
该类表示的是一个过滤器链。多个Filter节点被存入数组中,当请求来临时顺序调用。FilterChain对象的doFilter方法就表示将请求交给下一个Filter节点。而后服务器就会将请求交给下一个过滤器……如果没有下一个过滤器就会去访问目标资源。其具体执行过程如下图:
这个图将Filter双重过滤作用表达的很清晰明了。
- 对于请求:浏览器再发起一个请求的时候,这个请求被tomcat容器解析成对象,然后会将请求对象交给Filter,通过其中doFilter方法我们可以对其request进行修改,然后交给目标资源;
- 对于响应 :根据上图可以看到响应并不是由servlet直接发送给浏览器的,是在过滤器执行完之后才会将response发送给浏览器,我们可以借助Filter对response做一些处理。
5.Filter中重定向
示例一(过滤器执行完之后服务器才发送响应信息):
下面是我在网上看到的一个例子:
创建一个过滤器:
public class TestFilter extends HttpFilter {
private static final long serialVersionUID = -7045717568848767917L;
@Override
public void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
System.out.println("TestFilter中的线程:" + Thread.currentThread().getName());
System.out.println("response:" + response);
System.out.println("Start……");
response.sendRedirect(request.getContextPath() + "/test/main.jsp");
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("End……");
}
}
两个jsp页面
index.jsp:
<body>
<%
System.out.println("index.jsp");
%>
</body>
main.jsp
<body>
<%
System.out.println("main.jsp");
System.out.println("main.jsp中的线程:" + Thread.currentThread().getName());
%>
<%="你好" %>
</body>
TestFilter在xml中配置:
<filter>
<filter-name>TestFilter</filter-name>
<filter-class>com.sgg.test.test.TestFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>TestFilter</filter-name>
<url-pattern>/test/index.jsp</url-pattern>
</filter-mapping>
访问index.jsp之后控制台的现象:
从显示结果可以看到在Filter中进行重定向之后,并没有立刻输出main.jsp浏览器页面也没有展示“你好”的字样,而是等到End之后才输出显示。我自己觉得这个过程是这样的:浏览器先请求index.jsp,由于该页面需要经过过滤器,所以执行doFilter方法,虽然在该方法中进行了重定向,但是tomcat服务器并没有将这个response对象中封装的响应信息通过流发送给浏览器,而是等到过滤器执行完毕之后才将响应信息发送过去。 然后首先给浏览器发送302状态码并携带main.jsp的地址,接着浏览器重新发送一次请求(请求main.jsp),tomcat为其分配线程池中的线程与之会话,然后控制台才有“main.jsp”的输出。
示例二(重定向死循环):
基于上面的例子我们稍微改动一下web.xml中的配置,将main.jsp也添加到过滤范围内,如下:
<filter>
<filter-name>TestFilter</filter-name>
<filter-class>com.sgg.test.test.TestFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>TestFilter</filter-name>
<url-pattern>/test/index.jsp</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>TestFilter</filter-name>
<url-pattern>/test/main.jsp</url-pattern>
</filter-mapping>
继续访问index.jsp页面,产生如下现象:
可以看到造成了重定向的死循环,这是因为我们将main.jsp也加入到了过滤范围内(而且是同一个过滤器),所以在访问index.jsp时,先执行过滤器中方法,在过滤器中又重定向到main.jsp中,当过滤器执行完之后将这个response发送给浏览器,浏览器重新发起请求去访问main.jsp,又会经过这个过滤器……造成死循环。我觉得如果针对具体场景解决的话可以在重定向之前加上if判断。
6.多个Filter的执行特点
多个Filter过滤器一起执行的特点:
- 所有Filter和目标资源都默认在同一线程中;
- 多个Filter共同执行的时候,他们都使用同一个request对象;
- 如果使用web.xml对Filter进行配置,那么多个Filter的执行的优先顺序<filter-mapping>的由上到下的配置顺序决定的,先配置的先执行;
- 使用注解配置Filter的话,filter的执行顺序跟名称的字母顺序有关,例如AFilter会比BFilter先执行。
- 如果既在web.xml中配置了,也通过注解配置了,那么会优先执行web.xml中配置的Filter
7.实现HttpFilter
如上,我们一直实现Filter接口来开发Filter程序是比较不方便的,因为是通信协议是基于Http协议的,所以最基本我们需要将ServletRequest转化为HttpServletRequest,为了以后的方便,我们可以实现一个HttpFilter来简化以后开发,如下:
public abstract class HttpFilter implements Filter {
private FilterConfig filterConfig;
public void init(FilterConfig fConfig) throws ServletException {
this.filterConfig = fConfig;
init();
}
//子类可通过重写该方法完成初始化。其中filterConfig可通过其getter方法获取
protected void init() {};
public void destroy() {}
public FilterConfig getFilterConfig() {
return filterConfig;
}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
doFilter(req,resp,chain);
}
//为HTTP协议定制,子类必须实现该抽象方法
public abstract void doFilter(HttpServletRequest req,HttpServletResponse resp,FilterChain chain)throws IOException, ServletException;
}
8.过滤器应用案例
8.1编码过滤器
首先我们需要明确几种编码指的都是什么:比如下面的JSP的page指令:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<meta charset="UTF-8">
request.setCharacterEncoding("UTF-8")
response.setCharacterEncoding("UTF-8")
- pageEncoding属性:众所周知,JSP文件是要编译为Servlet的,该属性就是告诉JSP编译器在编译时使用的编码方式。
- contentType属性:在不设置response.setCharacterEncoding的情况下,指定服务器将该页面的响应信息以何种编码方式编码。
- <meta charset>:告诉浏览器以什么编码方式来读取翻译这个网页。
- request.setCharacterEncoding("UTF-8"):浏览器发送过来的请求以什么方式解码。如果不指定,则默认是ISO-8859-1。该方法对POST请求方式有效;对于GET请求方式需要如下做法:
String str1 = request.getParameter("name"); String str2 = new String(para.getBytes("iso-8859-1"),"utf-8");
- response.setCharacterEncoding("UTF-8"):指定服务器将响应信息以何种编码方式编码。
public class EncodingFilter extends HttpFilter{
private static final long serialVersionUID = -7245387469099990016L;
private String encoding;
@Override
public void init() {
//web.xml中利用<context-param>配置编码方式,在过滤器初始化时解析
encoding = getFilterConfig().getServletContext().getInitParameter("encoding");
}
@Override
public void doFilter(HttpServletRequest req, HttpServletResponse resp, FilterChain chain)
throws IOException, ServletException {
req.setCharacterEncoding(encoding);
chain.doFilter(req, resp);
resp.setCharacterEncoding(encoding);
}
}
8.2禁止浏览器缓存的过滤器
public class NoCacheFilter extends HttpFilter {
private static final long serialVersionUID = 6491222632609835068L;
@Override
public void doFilter(HttpServletRequest req, HttpServletResponse resp, FilterChain chain) throws IOException, ServletException {
//设置禁止浏览器缓存
resp.setDateHeader("Expires", -1);
resp.setHeader("Cache-Control", "no-cache");
resp.setHeader("Pragma", "no-cache");
chain.doFilter(req, resp);
}
}
8.3登录权限效验过滤器
两个html网页,分别是login.html和index.html。只有当用户为登录状态时才可以访问index.html。为index.html写了一个过滤器,login.html请求提交至LoginServlet。具体如下:
LoginServlet(效验账号密码,成功则生成用户对象并跳转页面,否则转到登录界面)
public class LoginServlet extends HttpServlet {
private static final long serialVersionUID = -82402018085132913L;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String userName = req.getParameter("userName");
String password = req.getParameter("password");
if("666".equals(userName) && "666".equals(password)) {
HttpSession session = req.getSession();
session.setAttribute("User", new Object());
resp.sendRedirect(req.getContextPath() + "/login/index.html");
}else {
resp.sendRedirect(req.getContextPath() + "/login/login.html");
}
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req, resp);
}
}
IsLoginFilter(如果session中存储了用户对象则说明此刻是登陆状态,否则就需要去登录界面登录)
public class IsLoginFilter extends HttpFilter {
private static final long serialVersionUID = -1893521777569775064L;
@Override
protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpSession session = request.getSession();
if(session.getAttribute("User") == null) {
response.sendRedirect(request.getContextPath() + "/login/login.html");
}else {
chain.doFilter(request, response);
}
}
}
二、监听器(Listener)
1.监听器概述
监听器是用于监听某个对象的状态变化的组件。比如监听ServletContext,HttpSession,ServletRequest等域对象的创建和销毁事件,还有这些域对象中的属性发生修改的事件。其中涉及到的几个名词如下:
- 事件源:被监听的对象。
- 监听器(listener):监听事件源对象状态的变化都会触发监听器。
- 注册监听器:将监听器与事件源对象进行绑定。
- 响应行为:事件源对象状态变化时的动作。
2.监听器分类
3.监听器的常用用途
- 统计在线人数
- Web应用启动时进行初始化操作。
- 统计网站访问量,可以监听ServletRequest域
- 跟Spring结合,做相关的操作。
4.监听器的使用
- 自定义监听器类去实现响应的接口,并重写接口中方法;(注意:由于一个web应用程序只会为每个事件监听器创建一个对象,有可能出现多个线程同时调用同一个事件监听器的情况,所以在编写事件监听器类时应考虑线程安全问题。)
- web.xml中对自定义监听器进行配置。
4.1针对对象的创建和销毁
public class HelloServletContextListener implements ServletContextListener {
//ServletContext域在web应用启动时被创建,所以当前web应用被启动时由Servlet容器调用该方法
@Override
public void contextInitialized(ServletContextEvent sce) {
//下面是获取被监听对象的两个方法
ServletContext sc = sce.getServletContext();
Object obj = sce.getSource();
}
//ServletContext域在web应用卸载时被销毁,所以当前web应用被卸载时由Servlet容器调用该方法
@Override
public void contextDestroyed(ServletContextEvent sce) {}
}
还需要去web.xml中配置,如下:
<listener>
<listener-class>com.sgg.Listener.test.HelloServletContextListener</listener-class>
</listener>
ServletContextListener是比较常用的,可在当前Web应用被加载时对当前Web应用进行相关资源进行初始化操作。如创建数据库连接池,创建IOC容器等。
HttpSessionListener接口也是一样的,其中有两个方法:
- sessionCreated方法:HttpSession对象被创建时调用(第一次调用request.getSession时)
- sessionDestroyed方法:HttpSession对象被销毁时调用(session过期,手动销毁,web应用被卸载)
利用HttpSessionListener可以实现统计在线人数,该类中可以维护一个AtomicInteger对象,当有人登录时服务器会创建session保存客户端登录信息,在sessionCreated方法中可以加1,在sessionDestroyed方法中减1。.
ServletRequestListener接口:
- requestInitialized方法:request对象被创建时调用(每次请求自动都会创建)。
- requestDestroyed方法:request对象被销毁时调用(请求结束时自动销毁)。
4.2针对域对象属性的增加、删除、替换
public class MyServletContextAttributeListener implements ServletContextAttributeListener {
@Override
public void attributeAdded(ServletContextAttributeEvent scae) {
System.out.println("新添加的键值对:" + scae.getName() + "\t" + scae.getValue());
}
@Override
public void attributeRemoved(ServletContextAttributeEvent scae) {
System.out.println("刚删除的键值对:" + scae.getName() + "\t" + scae.getValue());
}
@Override
public void attributeReplaced(ServletContextAttributeEvent scae) {
System.out.println("修改前的键值对:" + scae.getName() + "\t" + scae.getValue());
}
}
其他两个接口中也是这三个方法,同理。
4.3针对HttpSession
下面两个监听器不需要在web.xml中配置。
监听对象的绑定与解绑
对于某些POJO类,可以实现HttpSessionBindingListener接口来监听对象的绑定与解绑状态:
- 绑定状态:当前类对象存入session域中;
- 解绑状态:当前类对象从session域中移除。
例如:
public class Student implements HttpSessionBindingListener{
private String name;
private int age;
//省略getter和setter方法
@Override
public String toString() {
return "name=" + name + "age=" + age;
}
//当前对象被绑定到Session中时调用该方法
@Override
public void valueBound(HttpSessionBindingEvent event) {
System.out.println("本类对象被存入session:" + event.getName() + "\t" + event.getValue());
}
//当前对象从Session中移除时调用该方法
@Override
public void valueUnbound(HttpSessionBindingEvent event) {
System.out.println("本类对象从session中移除:" + event.getName() + "\t" + event.getValue());
}
}
编写一个JSP:
<%
Student stu = new Student();
stu.setName("张三");
stu.setAge(18);
session.setAttribute("student", stu);
session.removeAttribute("student");
%>
浏览器访问后控制台输出如下:
钝化与活化监听器
- session的序列化和反序列化:一般来说,服务器启动后就不在关闭了,但是如果迫不得已需要重启,而用户会话还在进行相应的操作,就需要将信息保存起来放在磁盘中。比如某个客户端正在进行访问服务器,但是服务器此时正常关闭了,那么tomcat就会将该客户端的session存储到磁盘中,当服务器重新启动时,可以通过反序列化还原出之前的session,里面可能包含用户的登录信息(session中的数据也要可序列化)等。(这些都是服务器自己完成的)
- 钝化与活化:当网站有大量用户访问时,服务器可能产生非常多的session,这会导致服务器的性能降低。当某个客户端超过一定的时间(可配置)没有与服务器交互,那么服务器就可以将session数据钝化(存储到磁盘中),等该用户需要重新使用的时候再活化(加载到内存)。这也是服务器帮做的。被钝化的数据也要能够被序列化。
我觉得这两者只不过是场景和概念上有点区别,其本质都是服务器帮我们进行序列化和反序列化操作。
JavaBean实现HttpSessionActivationListener接口可以监控自己被序列化(钝化)和反序列化(活化)。可以进行如下测试:
创建User类:
public class User implements Serializable, HttpSessionActivationListener{
private static final long serialVersionUID = -6812203570566039522L;
private String name;
private int age;
//省略getter和setter方法
@Override
public String toString() {
return "name: " + name + "age: " + age;
}
@Override
public void sessionWillPassivate(HttpSessionEvent se) {
System.out.println("当前对象被钝化");
}
@Override
public void sessionDidActivate(HttpSessionEvent se) {
System.out.println("当前对象被活化");
}
}
创建user.jsp:
<%
User user = new User();
user.setAge(20);
user.setName("王麻子");
session.setAttribute("user", user);
%>
启动服务器,浏览器访问user.jsp之后关闭服务器。
首先控制台会输出“当前对象被钝化”。由于我是用eclipse启动服务器的所以,在该目录下(F:\我的web学习\.metadata\.plugins\org.eclipse.wst.server.core\tmp0\work\Catalina\localhost\sgg_javaweb14_Listener)会有一个SESSIONS.ser文件出现。重新启动服务器,控制台显示“当前对象被活化”,而且SESSIONS.ser文件消失。
需要注意的是:
- 只有在服务器正常关闭的条件下,还未超时的Session 才会被序列化成文件。当Session 超时、调用invalidate 方法或者服务器在非正常情况下关闭时,Session 都不会被序列化,因此也就不存在反序列化。
- 生成SESSIONS.ser文件后不会因为超过Session 过期时间而消失,这个文件会一直存在,等到下一次服务器开启时消失。
- 当多个Session 被序列化时,这些被序列化的Session 都被保存在一个文件中,并不会为每个Session 都建立一个文件。
有关钝化和活化的时间需要在context.xml中进行配置。我找的路径是F:\我的web学习\.metadata\.plugins\org.eclipse.wst.server.core\tmp0\conf目录下的context.xml,在其中加上如下配置:
<Manager className="org.apache.catalina.session.PersistentManager" maxIdleSwap="5">
<Store className="org.apache.catalina.session.FileStore" directory="F:\mysession"> </Store>
</Manager>
<!--注意maxIdleSwap表示的是多久不活动会被钝化。 directory表示的是钝化后文件的存放路径-->
钝化后会在指定目录中产生如下文件:
通过监听器也可以发现。
参考https://blog.csdn.net/IPROMISE_LX/article/details/105810332?utm_source=app&app_version=4.5.0