Filter&Listener
目标:
- 能够使用
Filter
完成登录状态校验功能
# Filter
# Filter概述
Filter
表示过滤器,是JavaWeb三大组件(Servlet
、Filter
、Listener
)之一。Servlet
之前我们已经学习过,今天我们开始学习Filter
和Listener
。
过滤器可以把对资源的请求拦截下来,从而实现一些特殊的功能。
如下图所示,浏览器可以访问服务器上的所有资源(servlet
、JSP
、HTML
等)
在访问这些资源之前我们可以使用过滤器拦截一下,也就是说在访问资源之前会先经过Filter
,如下图
那么,拦截器拦截后可以用来做什么?
过滤器通常完成一些通用的操作。比如每个资源都要写一些代码来完成某个功能,这样的需求总不能在每个资源中都写同样的代码吧,这个时候我们可以将这些代码写在过滤器中,每个请求在请求资源的时候都要经过过滤器,也会执行我们写的代码。
之前我们做的品牌数据管理的案例中已经做了登录功能,那么,如果我们不登录能不能访问到数据呢?咱们来验证一下,使用浏览器直接访问首页index.jsp
,可以看到可以正常访问:
当我点击查询所有按钮后,居然可以看到品牌的数据
这显然和我们的要求不符,我们希望实现的效果是用户如果登录了就跳转到品牌数据展示的页面,如果没有登录就跳转到登录页面让用户登录。
要实现这个效果就需要在每一个资源中都写上这段逻辑,不过向这种通用的操作,我们可以放在过滤器中进行实现,在实际工作中这个就叫做权限控制,后面我们还会进一步进行细粒度的权限控制。除此之外,过滤器还可以做统一编码处理、敏感字符处理等等。
# Filter快速入门
开发步骤:
进行Filter
开发分成一下三步实现
- 定义类,实现
Filter
接口,并重写其所有方法
package com.itheima.filter;
import javax.servlet.*;
import java.io.IOException;
public class FilterDemo implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
}
@Override
public void destroy() {
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- 配置
Filter
拦截资源的路径:在雷声定义@WebFilter
注解,而注解的value
属性值/*
表示拦截所有的资源
package com.itheima.filter;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;
@WebFilter("/*")
public class FilterDemo implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
}
@Override
public void destroy() {
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
- 在
doFilter
方法中输出一句话,并放行
package com.itheima.filter;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;
@WebFilter("/*")
public class FilterDemo implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
System.out.println("filter 被执行了...");
// 放行
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
提示
上述代码中的filterChain.doFilter(servletRequest, servletResponse);
就是放行,也就是让请求访问本该被访问的资源。
# Filter执行流程
如上图是使用过滤器的流程,下面我们通过以下问题来研究过滤器的执行流程:
- 放行后访问对应资源,资源访问完成后,还会回到
Filter
中吗?从上图就可以看出肯定会回到Filter
中 - 如果回到
Filter
中,是重头执行还是执行放行后的逻辑呢?如果是从头执行的话,就意味着放行前逻辑会被执行两次,那一般人肯定不会这样设计,所以访问资源后,会回到放行后逻辑。
通过上述的说明,我们就可以总结Filter
的执行流程如下:
接下来我们就来通过代码验证一下,在doFilter()
方法前后都加上输出语句,如下
package com.itheima.filter;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;
@WebFilter("/*")
public class FilterDemo implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
System.out.println("filter 前逻辑 被执行了...");
// 放行
filterChain.doFilter(servletRequest, servletResponse);
System.out.println("filter 后逻辑 被执行了...");
}
@Override
public void destroy() {
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
重启服务器,访问登录页面http://localhost/course/login.jsp,在控制台打印内容如下:
由此我们后续就可以这样来操作,后面我们可以将对请求进行处理的代码放在放行之前进行处理,而如果请求完资源后还要对响应的数据进行处理就可以放在放行后进行处理。
# Filter拦截路径配置
拦截路径表示Filter
会对请求的哪些资源进行拦截,使用@WebFilter
注解进行配置,如@WebFilter("拦截路径")
拦截路径有如下四种配置方式:
- 拦截具体的资源:
/index.jsp
,只有访问index.jsp
时才会被拦截 - 目录拦截:
/user/*
,访问/user
下的所有资源,都会被拦截 - 后缀名拦截:
*.jsp
,访问后缀名为jsp
的资源,都会被拦截 - 拦截所有:
/*
,访问所有资源,都会被拦截
大家会发现拦截路径的配置和Servlet
的请求资源路径配置方式一样,但是表示的含义是不同的。
# 过滤器链
概述:过滤器链是指在一个Web应用,可以配置多个过滤器,这多个过滤器称为过滤器链。如下图就是一个过滤器链,我们学习过滤器链主要是学习过滤器链执行的流程
上图中的过滤器链执行是按照下面的流程执行:
- 执行
Filter1
的放行前逻辑代码 - 执行
Filter1
的放行代码 - 执行
Filter2
的放行前逻辑代码 - 执行
Filter2
的放行代码 - 访问资源
- 执行
Filter2
的放行后代码 - 执行
Filter1
的放行后代码
以上流程串起来就像一条链子,故称之为过滤器链。
代码演示:
- 编写第一个过滤器
FilterDemo
,配置成拦截所有资源
package com.itheima.filter;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;
@WebFilter("/*")
public class FilterDemo implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
System.out.println("1. FilterDemo 放行前逻辑被执行了...");
// 放行
filterChain.doFilter(servletRequest, servletResponse);
System.out.println("3. FilterDemo 放行后逻辑被执行了...");
}
@Override
public void destroy() {
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
- 编写第二个过滤器
FilterDemo2
,配置拦截所有资源
package com.itheima.filter;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;
@WebFilter("/*")
public class FilterDemo2 implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
// 1. 放行前,对request数据进行处理
System.out.println("2. FilterDemo2 放行前逻辑被执行了...");
filterChain.doFilter(servletRequest, servletResponse);
// 2. 放行后,对response数据进行处理
System.out.println("4. FilterDemo2 放行后逻辑被执行了...");
}
@Override
public void destroy() {
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
- 重启服务器,访问页面
从结果可以看到确实是按照我们之前说的执行流程进行的。
问题:上面代码中为什么先执行FilterDemo
,后执行FilterDemo2
?
因为我们使用的注解配置的Filter
,而这种配置方式的优先级是按照过滤器类名(字符串)的自然排序。比如有两个名称的过滤器:BFilterDemo
和AFilterDemo
。那一定是AFilterDemo
过滤器先执行。
# Filter案例
需求:访问服服务器资源时,需要先进行登录验证,如果没有登录,则自动跳转到登录页面。
分析:要实现该功能我们要在每一个资源里加入登录状态校验的代码吗?显然不需要,只需要写一个Filter
,在该过滤器中进行登录状态校验即可,该Filter
代码逻辑如下:
代码实现:
- 创建
Filter
:在工程中创建com.itheima.filter
包,在该包下创建名为LoginFilter
的过滤器
package com.itheima.filter;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;
@WebFilter("/*")
public class LoginFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
}
@Override
public void destroy() {
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
- 编写逻辑代码
在doFilter()
方法中编写登录状态校验的逻辑代码
首先需要从session
对象中获取用户信息,但是ServletRequest
类型的request
对象没有获取session
对象的方法,所以此时需要将request
对象强制转换为HttpServletRequest
对象。
HttpServletRequest request = (HttpServletRequest) servletRequest;
然后再完成以下逻辑
- 获取
Session
对象 - 从
Session
对象中获取名为user
的数据 - 判断获取到的数据是否是
null
- 如果不是,说明已经登录,放行
- 如果是,说明未登录,将提示信息存储到域对象中并跳转到登录页面
代码如下:
package com.itheima.filter;
import com.itheima.pojo.User;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.io.IOException;
@WebFilter("/*")
public class LoginFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
// 1. 判断session中是否有user
HttpSession session = request.getSession();
User user = (User) session.getAttribute("user");
// 2. 判断user是否为null
if (user != null) {
// 已登录 放行
filterChain.doFilter(request, servletResponse);
} else {
// 没有登录,存储提示信息,跳转到登录页面
request.setAttribute("login_msg", "您尚未登录!");
request.getRequestDispatcher("/login.jsp").forward(request, servletResponse);
}
}
@Override
public void destroy() {
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
- 测试并抛出问题
在浏览器上输入http://localhost/course/index.jsp,可以看到如下页面效果
从上面效果可以看出没有登录确实跳转到登录页面了,但是登录页面为什么展示成这种效果了呢?
- 问题分析及解决
因为登录页面需要css/login.css
这个文件进行样式的渲染,下图就是登录页面引入css文件的位置
而在请求这个css资源时被过滤器拦截,就相当于没有加载到样式文件导致的,解决这个问题,只需要对所有登录相关的资源进行放行即可。还有一种情况就是当新用户进行注册时,注册页面同样也需要被过滤器放行。
综上,我们需要在判断session
中是否包含用户信息之前,应该加上对登录及注册相关资源放行的逻辑处理
package com.itheima.filter;
import com.itheima.pojo.User;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.io.IOException;
@WebFilter("/*")
public class LoginFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
// 判断访问资源路径是否和登录注册相关
// 在数组中存储登录和注册相关的资源路径
String[] urls = {"/login.jsp", "/imgs/", "/css/", "/loginServlet", "/register.jsp", "registerServlet", "/checkCodeServlet"};
// 获取当前访问的资源路径
String url = request.getRequestURL().toString();
// 遍历数组,获取每一个需要放行的资源路径
for (String u : urls) {
// 判断当前访问的资源路径字符串是否包含要放行的资源路径字符串
// 比如当前访问的资源路径时/course/login.jsp
// 而字符串/course/login.jsp包含了字符串/login.jsp,所以这个字符串就需要放行
if (url.contains(u)) {
// 包含,放行
filterChain.doFilter(request, servletResponse);
return;
}
}
// 1. 判断session中是否有user
HttpSession session = request.getSession();
User user = (User) session.getAttribute("user");
// 2. 判断user是否为null
if (user != null) {
// 已登录 放行
filterChain.doFilter(request, servletResponse);
} else {
// 没有登录,存储提示信息,跳转到登录页面
request.setAttribute("login_msg", "您尚未登录!");
request.getRequestDispatcher("/login.jsp").forward(request, servletResponse);
}
}
@Override
public void destroy() {
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# Listener
# Listener概述
Listener
表示监听器,是JavaWeb三大组件(Servlet、Filter、Listener)之一- 监听器可以监听
application
,session
,request
三个对象创建、销毁或者往其中添加、修改、删除属性时自动执行业务代码的功能组件。
request
和session
我们学习过,而application
是ServletContext
类型的对象。
ServletContext
代表整个Web
应用,在服务器启动的时候,Tomcat会自动创建该对象,在服务器关闭时会自动销毁该对象。
# Listener分类
JavaWeb提供了8个监听器:
这里面只有ServletContextListener
这个监听器后期我们会接触到,ServletContextListener
是用来监听ServletContext
对象的创建和销毁。
ServletContextListener
接口中有以下两个方法:
void contextInitialized(ServletContextEvent sce);
ServletContext
对象被创建会自动执行的方法void contextDestroyed(ServletContextEvent sce);
ServletContext
对象被销毁时自动执行的方法
# Listener代码演示
步骤:
- 定义一个类,实现
ServletContextListener
接口 - 重写所有的抽象方法
- 使用
@WebListener
进行配置
代码如下:
package com.itheima.listener;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
@WebListener
public class ContextLoaderListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent servletContextEvent) {
// 加载资源
System.out.println("ContextLoaderListener...");
}
@Override
public void contextDestroyed(ServletContextEvent servletContextEvent) {
// 释放资源
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
启动服务器,就可以在启动的日志信息中看到contextInitialized()
方法输出的内容,同时也说明了ServletContext
对象在服务器启动的时候被创建了。