Filter 介绍

过滤器,顾名思义就是对事物进行过滤的,在Web中的过滤器,当然就是对请求进行过滤,对web服务器管理的所有web资源,我们使用过滤器,就可以对请求进行拦截,然后做相应的处理,实现许多特殊功能。如登录控制,权限管理,过滤敏感词汇等.

Servlet API中提供了一个Filter接口,开发web应用时,如果编写的Java类实现了这个接口,则把这个java类称之为过滤器Filter。通过 Filter 技术,开发人员可以实现用户在访问某个目标资源之前,对访问的请求和响应进行拦截,Filter接口源代码:

1
2
3
4
5
6
public abstract interface Filter{
public abstract void init(FilterConfig paramFilterConfig) throws ServletException;
public abstract void doFilter(ServletRequest paramServletRequest, ServletResponse paramServletResponse, FilterChain
paramFilterChain) throws IOException, ServletException;
public abstract void destroy();
}

Filter接口中有一个doFilter方法,当我们编写好Filter,并配置对哪个web资源进行拦截后,WEB服务器每次在调用web资源的service方法之前,都会先调用一下filter的doFilter方法,因此,在该方法内编写代码可达到如下目的: - 调用目标资源之前,让一段代码执行。 - 是否调用目标资源(即是否让用户访问web资源)。

web服务器在调用doFilter方法时,会传递一个filterChain对象进来,filterChain对象是filter接口中最重要的一个对象,它也提供了一个 doFilter方法,开发人员可以根据需求决定是否调用此方法,调用该方法,则web服务器就会调用web资源的service方法,即web资源就会被访问,否则web资源不会被访问。

过滤器原理

当我们使用过滤器时,过滤器会对游览器的请求进行过滤,过滤器可以动态的分为3个部分

1.放行之前的代码,2.放行,3.放行后的代码

  • 第一部分代码会对游览器请求进行第一次过滤,然后继续执行
  • 第二部分代码就是将游览器请求放行,如果还有过滤器,那么就继续交给下一个过滤器
  • 第三部分代码就是对返回的Web资源再次进行过滤处理

我们使用过滤器,也就是说,不止请求会经过过滤器,我们的响应也会经过过滤器。

img
img

使用过滤器

我们创建Filter,只需要继承Filter接口就行。

1
2
3
4
5
6
7
8
9
10
11
12
13
import javax.servlet.*;
import java.io.IOException;

@WebFilter("/*")
public class MyFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("对request进行过滤");
//下面这行代码就是放行
filterChain.doFilter(servletRequest,servletResponse);
System.out.println("对response进行过滤");
}
}

我们实现了一个 doFilter 方法,这个方法就是我们写过滤代码的地方,具体逻辑就是和上面介绍的过滤器原理一样的。

我简单介绍下上面的代码,WebFilter("/*") 表示对所有请求进行过滤,而在doFilter中的放行代码,也就是filterChain.doFilter(servletRequest,servletResponse);这行代码就是对拦截进行放行

过滤器并不会管资源是否存在,而只会对配置的拦截路径进行拦截。拦截不仅会对请求进行拦截,而且还会对相应进行拦截。

配置过滤器拦截路径

配置Filter的拦截路径有2种方式,一种是注解,一种是xml方式,我们分别进行讲解

注解方式

我们如果使用注解来进行配置,那么我们就需要使用 @WebFilter ,直接看该注解的源码

img
img
  • filterName:该filter的名字
  • initParams:初始化参数
  • displayName:filter显示名称
  • servletNames:指定对哪些servlet进行过滤
  • asyncSupported:是否支持异步模式
  • urlPatterns:指定拦截路径
  • value:指定拦截路径

urlPatterns和value是一样的。urlPatterns和value只能配置一个,不能两个都配置,两个都配置就会报错。

对于使用@WebFilter,里面的多个参数用 , 进行分隔。

如果我们仅仅需要配置一个拦截路径,那么我们可以直接简写@WebLister(“拦截路径”),如@WebFilter(“/*”)就是拦截所有请求。

1
2
@WebFilter(value = {"/login"}, filterName = "filtersss")
public class CharSetFilter implements Filter {

多个参数配置,需要分隔

xml 方式

xml方式可以说是和Servlet使用xml配置方式一样了

1
2
3
4
5
6
7
8
<filter>
<filter-name>myFilter</filter-name>
<filter-class>com.ergoutree.filter.MyFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>myFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

把注解换成了xml标签来配置,里面属性都是一样的,这个和Servlet的配置方式基本一样

过滤器生命周期

Filter 的生命周期和 Servlet 也十分相似

创建一个类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import javax.servlet.*;
import java.io.IOException;

public class LifeCycleFilter 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() {
}
}

Filter有3个阶段,分别是初始化,拦截和过滤,销毁。

  • 初始化阶段:当服务器启动时,我们的服务器(Tomcat)就会读取配置文件,扫描注解,然后来创建我们的Filter。
  • 拦截和过滤阶段:只要请求资源的路径和拦截的路径相同,那么过滤器就会对请求进行过滤,这个阶段在服务器运行过程中会一直循环。
  • 销毁阶段:当服务器(Tomcat)关闭时,服务器创建的Filter也会随之销毁。

Filter的三个阶段就对应着Filter的3个方法,init 方法会在 Filter 创建时调用,doFilter 方法会在请求和拦截匹配时调用,destroy 方法会在 Filter 销毁时调用

1
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
@WebFilter(value = {"/login"}, filterName = "filtersss")
public class CharSetFilter implements Filter {
private static final Logger LOGGER = Logger.getLogger(CharSetFilter.class.getName());
private static final String CHARSET = "UTF-8";

public void init(FilterConfig config) throws ServletException {
System.out.println("LoginOccasion过滤器初始化完成 - " + new Date());
}

public void destroy() {
System.out.println("LoginOccasion过滤器被销毁 - " + new Date());
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
// 1. 预处理 - 在请求到达Servlet之前执行
System.out.println("LoginOccasion过滤器: 预处理开始");

// 获取客户端IP地址
String ipAddress = request.getRemoteAddr();
System.out.println("请求来自IP: " + ipAddress + " - " + new Date());

request.setCharacterEncoding(CHARSET);
response.setCharacterEncoding(CHARSET);
response.setContentType("text/html; charset=" + CHARSET);

if(request instanceof HttpServletRequest) {
HttpServletRequest httpRequest = (HttpServletRequest) request;
System.out.println("访问的URL: " + httpRequest.getRequestURL());
System.out.println("HTTP方法: " + httpRequest.getMethod());
}

// 记录请求开始时间
long startTime = System.currentTimeMillis();

// 2. 将请求传递给下一个过滤器或目标Servlet
chain.doFilter(request, response);

// 3. 后处理 - 在响应返回客户端之前执行
long endTime = System.currentTimeMillis();
System.out.println("请求处理耗时: " + (endTime - startTime) + "毫秒");
System.out.println("LoginOccasion过滤器: 后处理完成");
}
}

FilterConfig和FilterChain

FilterConfigFilterConfig这2个对象是由服务器(Tomcat)在创建和调用Filter对象时所传入的,这2个对象十分有用,FilterConfig对象可以读取我们配置的初始参数,FilterChain可以实现多个Filter之间的连接。

FilterConfig

img
img
img
img

里面的方法就4个,下面我们分别进行讲解

  • getFilterName():获取filter的名称
  • getServletContext():获取ServletContext
  • getInitparamter(String var1):获取配置的初始参数的值
  • getInitParamterNames():获取配置的所有参数名称

实际例子

我们在init方法中使用FilterConfig来读取配置的数据库的信息,然后输出。

1
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
import javax.servlet.*;
import java.io.IOException;
import java.util.Enumeration;

public class MyFilterConfig implements Filter {

@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("-----------获取全部key:value------------");
//得到所有配置参数的名字
Enumeration<String> names = filterConfig.getInitParameterNames();
while (names.hasMoreElements()) {
//得到每一个名字
String name = names.nextElement();
System.out.println(name+" = "+filterConfig.getInitParameter(name));
}
System.out.println("-----------end.....------------");
}

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
}

@Override
public void destroy() {
}
}

xml 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<filter>
<filter-name>myFilterConfig</filter-name>
<filter-class>com.clucky.filter.MyFilterConfig</filter-class>
<init-param>
<param-name>driver</param-name>
<param-value>com.mysql.jdbc.Driver</param-value>
</init-param>
<init-param>
<param-name>url</param-name>
<param-value>jdbc:mysql://localhost:3306/equip_employ_manage?serverTimezone=GMT</param-value>
</init-param>
<init-param>
<param-name>username</param-name>
<param-value>root</param-value>
</init-param>
<init-param>
<param-name>password</param-name>
<param-value>root</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>myFilterConfig</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
img
img

我们使用FilterConfig提供的方法就成功实现了功能,FilterConfig就是用来读取配置文件的。

FilterChain

在一个web应用中,可以开发编写多个Filter,这些Filter组合起来称之为一个Filter链。web服务器根据Filter在 web.xml 文件中的注册顺序,决定先调用哪个Filter,当第一个 Filter 的 doFilter 方法被调用时,web服务器会创建一个代表 Filter 链的FilterChain 对象传递给该方法。在 doFilter 方法中,开发人员如果调用了 FilterChain 对象的 doFilter 方法,则 web 服务器会检查 FilterChain 对象中是否还有filter,如果有,则调用第2个filter,如果没有,则调用目标资源。

img
img
img
img

我们查看类图,可以发现 FilterChain 就只有一个方法,其实这个方法就是用来对拦截进行放行的,如果有多个拦截器,那么就会继续调用下一个Filter进行拦截。doFilter 方法需要传入个参数,一个是 ServletRequest ,一个是 ServletResponse 参数,这个直接传入进行。

Tomcat 在调用过滤器时,默认就会传入 Request 和 Response,这个参数封装了请求和响应,我们直接使用就行。ServletResquest 和 ServletResponse 可以直接强转成 HttpServletRequest 和 HttpServletResponse,然后使用相应的方法。

将ServletRequest转成HttpServletRequest

img
img

应用实例

我们前面一直都是一个Filter,现在我们来配置2个Filter,通过FilterChain来进行多个过滤。

第一个 Filter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;

@WebFilter("/*")
public class Filter01 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("调用过滤器01对请求进行过滤~~~~");
//放行,如果还有过滤器,那么就执行下一个过滤器
filterChain.doFilter(servletRequest,servletResponse);
System.out.println("调用过滤器01对响应进行过滤~~~~");
}

@Override
public void destroy() {
}
}

第二个Filterl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;

@WebFilter("/*")
public class Filter02 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("调用过滤器02对请求进行过滤~~~~");
//放行,如果还有过滤器,那么就执行下一个过滤器
filterChain.doFilter(servletRequest,servletResponse);
System.out.println("调用过滤器02对响应进行过滤~~~~");
}

@Override
public void destroy() {
}
}

启动服务器,访问8080,查看控制台输出。

img
img

我们可以看见Filter01先进行过滤,然后交给Filter02,然后访问资源,然后Filter02对响应进行过滤,然后Filter01对响应进行过滤。图示如下:

img
img

多个 Filter 的执行顺序

上面我们配置了2个过滤器,那么我们怎么知道那个过滤器先执行呢?

  • 如果我们是在web.xml中配置的过滤器,那么过滤器的执行顺序就是在web配置的顺序,配置在上面那么就会先执行。
  • 如果我们是使用@WebFilter进行配置的,那么执行顺序就是字符比较顺序来执行,例如有2个过滤器,一个是AFilter,一个是BFilter,那么AFilter就会先执行。
  • 如果注解和xml混用,那么在web.xml中配置的会先执行。

Filter应用实例—实现敏感词汇过滤

我们写一个评论页面,可以进行评论,如果评论中含有我们定义的敏感词汇,那么我们就进行过滤,使用星号来进行代替。

首先是一个简单的 jsp 页面

1
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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%-- 引入JSTL核心标签库,简化页面数据处理(需确保项目已引入JSTL依赖) --%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<head>
<title>评论系统 - 敏感词汇过滤</title>
<style>
/* 简单样式优化,提升页面体验 */
.container {
width: 800px;
margin: 50px auto;
padding: 20px;
border: 1px solid #eee;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0,0,0,0.05);
}
textarea {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
resize: vertical;
margin-bottom: 15px;
}
input[type="submit"] {
padding: 8px 20px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
input[type="submit"]:hover {
background-color: #45a049;
}
.result {
margin-top: 20px;
padding: 15px;
border-radius: 4px;
background-color: #f9f9f9;
}
.success {
color: #4CAF50;
font-weight: bold;
}
.warning {
color: #f44336;
font-weight: bold;
}
</style>
</head>
<body>
<div class="container">
<h1>评论提交</h1>
<%-- 评论表单:提交到/comment接口,POST方式提交(更安全,避免参数暴露在URL) --%>
<form action="${pageContext.request.contextPath}/comment" method="post">
<label for="message">评论内容:</label>
<textarea id="message" name="message" cols="30" rows="8" placeholder="请输入评论内容(敏感词汇将被过滤)" required></textarea>
<input type="submit" value="提交评论">
</form>

<%-- 过滤结果展示区域:仅当有评论结果时显示 --%>
<c:if test="${not empty requestScope.comment}">
<div class="result">
<p>
<%-- 根据是否有敏感词,显示不同前缀样式 --%>
<span class="${requestScope.hasSensitiveWord ? 'warning' : 'success'}">
${requestScope.name}
</span>
您的评论(已过滤敏感词):<br>
${requestScope.comment}
</p>
<%-- 若有敏感词,提示用户 --%>
<c:if test="${requestScope.hasSensitiveWord}">
<p class="warning">提示:您的评论中包含敏感词汇,已自动替换处理</p>
</c:if>
</div>
</c:if>
</div>
</body>
</html>

然后是Filter代码

1
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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
package com.example.filter;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.annotation.WebInitParam;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;

/**
* 敏感词汇过滤Filter
* 功能:拦截评论请求,过滤请求参数中的敏感词汇,将敏感词替换为**
* 配置:通过@WebInitParam配置多个敏感词,用英文逗号分隔
* 拦截目标:拦截所有访问/comment接口的请求(与Servlet的urlPattern匹配)
*/
@WebFilter(
urlPatterns = {"/comment"}, // 拦截目标:仅拦截评论提交接口,精准拦截避免资源浪费
initParams = {
// 初始化参数:敏感词汇列表,支持配置多个(用英文逗号分隔)
@WebInitParam(name = "sensitiveWords", value = "zz,垃圾,脏话,敏感词1,敏感词2")
},
dispatcherTypes = {DispatcherType.REQUEST} // 仅拦截直接请求(不拦截转发/包含等)
)
public class SensitiveWordFilter implements Filter {

// 存储敏感词汇的集合(用List便于遍历,也可改用HashSet提升查询效率)
private List<String> sensitiveWordList;

/**
* Filter初始化方法:服务器启动时执行一次,初始化敏感词汇列表
* @param filterConfig Filter配置对象,用于获取初始化参数
*/
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// 1. 获取初始化参数中的敏感词汇(配置在@WebInitParam中)
String sensitiveWordsStr = filterConfig.getInitParameter("sensitiveWords");

// 2. 校验参数有效性,避免空指针异常
if (Objects.isNull(sensitiveWordsStr) || sensitiveWordsStr.trim().isEmpty()) {
throw new ServletException("敏感词汇配置不能为空!请在@WebInitParam中配置sensitiveWords参数");
}

// 3. 将字符串按英文逗号分割,转换为List集合(去重处理,避免重复过滤)
String[] sensitiveWordArr = sensitiveWordsStr.split(",");
sensitiveWordList = Arrays.stream(sensitiveWordArr)
.map(String::trim) // 去除每个敏感词的前后空格
.distinct() // 去重:避免配置重复敏感词导致多次替换
.toList();

// 4. 打印初始化日志(便于调试,确认敏感词加载成功)
System.out.println("[SensitiveWordFilter] 初始化完成,加载敏感词汇:" + sensitiveWordList);
}

/**
* Filter核心过滤方法:每次拦截请求时执行,处理敏感词汇过滤
* @param request 请求对象:用于获取评论参数、设置属性传递给后续组件
* @param response 响应对象:用于设置编码,避免中文乱码
* @param chain 过滤器链:用于放行请求到下一个Filter或目标Servlet
*/
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 1. 统一设置请求/响应编码:避免中文乱码(必须在获取参数前设置)
request.setCharacterEncoding("UTF-8");
response.setContentType("text/html;charset=UTF-8");
response.setCharacterEncoding("UTF-8");

// 2. 获取评论参数(从请求中获取name为message的表单值)
String originalComment = request.getParameter("message");
// 校验评论内容不为空(空评论无需过滤,直接放行)
if (Objects.isNull(originalComment) || originalComment.trim().isEmpty()) {
chain.doFilter(request, response);
return;
}

// 3. 执行敏感词汇过滤:遍历敏感词列表,替换所有敏感词
String filteredComment = originalComment;
boolean hasSensitiveWord = false; // 标记是否包含敏感词
for (String sensitiveWord : sensitiveWordList) {
// 仅当评论包含当前敏感词时才执行替换(避免无意义操作)
if (filteredComment.contains(sensitiveWord)) {
hasSensitiveWord = true;
// 替换逻辑:用**替换敏感词(可改为按敏感词长度生成对应数量的*,更严谨)
filteredComment = filteredComment.replace(sensitiveWord, "**");
// 日志记录:便于追踪敏感词使用情况
System.out.println("[SensitiveWordFilter] 过滤敏感词:" + sensitiveWord
+ ",原始内容:" + originalComment + ",过滤后:" + filteredComment);
}
}

// 4. 将过滤结果存入request域:传递给后续的Servlet或JSP使用
request.setAttribute("originalComment", originalComment); // 原始评论(便于对比)
request.setAttribute("comment", filteredComment); // 过滤后评论(用于展示)
request.setAttribute("hasSensitiveWord", hasSensitiveWord); // 是否含敏感词标记

// 5. 放行请求:让请求继续流向目标Servlet(CommentServlet)
chain.doFilter(request, response);
}

/**
* Filter销毁方法:服务器关闭时执行,用于释放资源(当前无资源需释放,仅作示例)
*/
@Override
public void destroy() {
// 若后续添加了文件流、数据库连接等资源,需在此处关闭
System.out.println("[SensitiveWordFilter] 销毁完成");
}
}

Servlet代码

1
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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
package com.example.servlet;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashSet;
import java.util.Objects;

/**
* 评论处理Servlet
* 功能:接收Filter过滤后的评论,判断是否含敏感词,记录敏感词IP,转发结果到JSP
* 访问路径:/comment(与Filter的拦截路径一致)
*/
@WebServlet(
name = "CommentServlet",
urlPatterns = {"/comment"}, // 访问路径:与前端表单的action对应
loadOnStartup = 1 // 服务器启动时初始化Servlet(默认是第一次访问时初始化)
)
public class CommentServlet extends HttpServlet {

// 存储发送敏感词的IP地址集合(用HashSet自动去重,避免同一IP重复记录)
// 注意:此处用内存存储,服务器重启后数据会丢失;生产环境需改用数据库/Redis持久化
private HashSet<String> sensitiveIpSet = new HashSet<>();

/**
* 处理GET请求:本示例中GET和POST请求逻辑一致,统一委托给doPost处理
* (实际开发中GET请求建议用于查询,POST用于提交,此处为简化示例)
*/
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doPost(request, response);
}

/**
* 处理POST请求:核心业务逻辑,判断敏感词、记录IP、转发结果
*/
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 1. 从request域中获取Filter传递的参数(Filter已处理敏感词过滤)
String originalComment = (String) request.getAttribute("originalComment");
String filteredComment = (String) request.getAttribute("comment");
boolean hasSensitiveWord = (boolean) request.getAttribute("hasSensitiveWord");

// 2. 校验参数有效性(避免Filter未执行导致的空值异常)
if (Objects.isNull(originalComment) || Objects.isNull(filteredComment)) {
request.setAttribute("name", "错误:");
request.setAttribute("comment", "评论内容处理失败,请重试!");
// 转发到评论页面展示错误信息
request.getRequestDispatcher("/comment.jsp").forward(request, response);
return;
}

// 3. 业务逻辑处理:根据是否含敏感词,设置不同提示信息和记录IP
if (hasSensitiveWord) {
// 3.1 含敏感词:记录IP,设置"bad boy"提示
String clientIp = getClientRealIp(request); // 获取客户端真实IP(处理代理场景)
sensitiveIpSet.add(clientIp); // 记录IP(自动去重)
request.setAttribute("name", "注意:");
// 日志记录:便于运维监控敏感词IP
System.out.println("[CommentServlet] 检测到敏感词,客户端IP:" + clientIp
+ ",当前敏感词IP总数:" + sensitiveIpSet.size());
} else {
// 3.2 不含敏感词:设置"good boy"提示
request.setAttribute("name", "成功:");
System.out.println("[CommentServlet] 评论无敏感词,内容:" + originalComment);
}

// 4. 请求转发:将结果转发到comment.jsp页面展示(保持URL不变,避免表单重复提交)
// 注意:此处用转发(forward)而非重定向(redirect),因为需要传递request域中的数据
request.getRequestDispatcher("/comment.jsp").forward(request, response);
}

/**
* 工具方法:获取客户端真实IP(处理反向代理、负载均衡场景,如Nginx代理)
* @param request 请求对象
* @return 客户端真实IP地址
*/
private String getClientRealIp(HttpServletRequest request) {
// 1. 先从代理头中获取IP(如Nginx配置的X-Real-IP、X-Forwarded-For)
String ip = request.getHeader("X-Real-IP");
if (Objects.isNull(ip) || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Forwarded-For");
}
// 2. 若代理头无值,从请求中获取远程IP
if (Objects.isNull(ip) || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
// 3. 处理X-Forwarded-For多IP场景(第一个IP为真实客户端IP)
if (ip != null && ip.contains(",")) {
ip = ip.split(",")[0].trim();
}
return ip;
}
}

我们输入http://localhost:8080/filter/comment.jsp,来访问jsp页面