过滤器&监听器

目录

一、过滤器(Filter)

1.Filter过滤器简介

2.自定义Filter

3.FilterConfig类

4.FilterChain类

5.Filter中重定向

6.多个Filter的执行特点

7.实现HttpFilter

8.过滤器应用案例

8.1编码过滤器

8.2禁止浏览器缓存的过滤器

8.3登录权限效验过滤器

二、监听器(Listener)

1.监听器概述

2.监听器分类

3.监听器的常用用途 

4.监听器的使用 

4.1针对对象的创建和销毁

4.2针对域对象属性的增加、删除、替换

4.3针对HttpSession


一、过滤器(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");
	}
}
  1. init(FilterConfig)方法:初始化自定义Filter,它与Servlet的init方法作用是一样的,FilterConfig于ServletConfig也类似,利用这两个对象都可以获得容器的环境类ServletContext对象,FilterConfig也可以取到<filter>下配置的<init-param>参数值。注意web工程启动时就会执行其构造方法和init(FilterConfig)方法。(Filter已创建和初始化完成)
  2. doFilter(ServletRequest,ServletResopnse,FilterChain)方法:在每个用户请求进来时这个方法都会被调用,并且在Servlet的service方法之前被调用。而FilterChain就代表当前的整个请求链,所以通过调用FilterChain对象的doFilter方法可以将请求传递给下一个过滤器。如果想拦截这个请求,可以不调用FilterChain对象的doFilter方法,那么这个请求将被直接返回。这是一种职责链模式。
  3. 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。这四种取值的意思如下:

  1. REQUEST:只要发起的操作是一次HTTP请求,比如请求某个URL、发起了一个GET请求、表单提交方式为POST的POST请求、表单提交方式为GET的GET请求。一次重定向则前后相当于发起了两次请求,这些情况下有几次请求就会走几次指定过滤器。  如果目标资源是通过RequestDispatcher的include()或forward()方法访问时,那么该过滤器就不会被调用
  2. FORWARD:如果目标资源是通过请求转发的形式访问时该过滤器才会被调用,(RequestDispatcher的forward()方法,或< jsp:forward page="……" /> 或 page指令的errorPage转发页面.<%@ page errorPage=“……” %>),除此之外,该过滤器不会被调用。
  3. INCLUDE:如果目标资源是通过RequestDispatcher的include()方法(或 < jsp:include file="/…" />)访问时,那么该过滤器将被调用。除此之外,该过滤器不会被调用。
  4. ERROR:如果目标资源是通过声明式异常处理机制调用时(web.xml中配置了<error-page></error-page>),那么该过滤器将被调用。除此之外,过滤器不会被调用。

3.FilterConfig类

正如上面提到的它与ServletConfig类似,它是每个自定义Filter的配置类。Tomcat 每次创建 Filter 的时候,也会同时创建一个 FilterConfig 类,这里包含了 Filter 配置文件的配置信息。通过该对象可以获取Filter的配置信息:

  1. 获取 Filter 的名称 filter-name 的内容
  2. 获取在 Filter 中配置的 init-param 初始化参数值
  3. 获取 ServletContext 对象

4.FilterChain类

该类表示的是一个过滤器链。多个Filter节点被存入数组中,当请求来临时顺序调用。FilterChain对象的doFilter方法就表示将请求交给下一个Filter节点。而后服务器就会将请求交给下一个过滤器……如果没有下一个过滤器就会去访问目标资源。其具体执行过程如下图:

这个图将Filter双重过滤作用表达的很清晰明了。

  1. 对于请求:浏览器再发起一个请求的时候,这个请求被tomcat容器解析成对象,然后会将请求对象交给Filter,通过其中doFilter方法我们可以对其request进行修改,然后交给目标资源;
  2. 对于响应 :根据上图可以看到响应并不是由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过滤器一起执行的特点:

  1. 所有Filter和目标资源都默认在同一线程中;
  2. 多个Filter共同执行的时候,他们都使用同一个request对象
  3. 如果使用web.xml对Filter进行配置,那么多个Filter的执行的优先顺序<filter-mapping>的由上到下的配置顺序决定的,先配置的先执行;
  4. 使用注解配置Filter的话,filter的执行顺序跟名称的字母顺序有关,例如AFilter会比BFilter先执行。
  5. 如果既在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")
  1. pageEncoding属性:众所周知,JSP文件是要编译为Servlet的,该属性就是告诉JSP编译器在编译时使用的编码方式。
  2. contentType属性:在不设置response.setCharacterEncoding的情况下,指定服务器将该页面的响应信息以何种编码方式编码。
  3. <meta charset>:告诉浏览器以什么编码方式来读取翻译这个网页。
  4. 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");

     

  5. 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等域对象的创建和销毁事件,还有这些域对象中的属性发生修改的事件。其中涉及到的几个名词如下:

  1. 事件源:被监听的对象。
  2. 监听器(listener):监听事件源对象状态的变化都会触发监听器。
  3. 注册监听器:将监听器与事件源对象进行绑定。
  4. 响应行为:事件源对象状态变化时的动作。

2.监听器分类

3.监听器的常用用途 

  1. 统计在线人数
  2. Web应用启动时进行初始化操作。
  3. 统计网站访问量,可以监听ServletRequest域
  4. 跟Spring结合,做相关的操作。

4.监听器的使用 

  1. 自定义监听器类去实现响应的接口,并重写接口中方法;(注意:由于一个web应用程序只会为每个事件监听器创建一个对象,有可能出现多个线程同时调用同一个事件监听器的情况,所以在编写事件监听器类时应考虑线程安全问题。)
  2. 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接口也是一样的,其中有两个方法:

  1. sessionCreated方法:HttpSession对象被创建时调用(第一次调用request.getSession时)
  2. sessionDestroyed方法:HttpSession对象被销毁时调用(session过期,手动销毁,web应用被卸载)

利用HttpSessionListener可以实现统计在线人数,该类中可以维护一个AtomicInteger对象,当有人登录时服务器会创建session保存客户端登录信息,在sessionCreated方法中可以加1,在sessionDestroyed方法中减1。.

ServletRequestListener接口

  1. requestInitialized方法:request对象被创建时调用(每次请求自动都会创建)。
  2. 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接口来监听对象的绑定与解绑状态:

  1. 绑定状态:当前类对象存入session域中;
  2. 解绑状态:当前类对象从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");
%>

浏览器访问后控制台输出如下:

钝化与活化监听器

  1. session的序列化和反序列化:一般来说,服务器启动后就不在关闭了,但是如果迫不得已需要重启,而用户会话还在进行相应的操作,就需要将信息保存起来放在磁盘中。比如某个客户端正在进行访问服务器,但是服务器此时正常关闭了,那么tomcat就会将该客户端的session存储到磁盘中,当服务器重新启动时,可以通过反序列化还原出之前的session,里面可能包含用户的登录信息(session中的数据也要可序列化)等。(这些都是服务器自己完成的)
  2. 钝化与活化:当网站有大量用户访问时,服务器可能产生非常多的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文件消失。

需要注意的是:

  1. 只有在服务器正常关闭的条件下,还未超时的Session 才会被序列化成文件。当Session 超时、调用invalidate 方法或者服务器在非正常情况下关闭时,Session 都不会被序列化,因此也就不存在反序列化。
  2. 生成SESSIONS.ser文件后不会因为超过Session 过期时间而消失,这个文件会一直存在,等到下一次服务器开启时消失。
  3. 当多个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

https://www.cnblogs.com/nick-huang/p/5672298.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值