# Java Web 高级
# 1. JavaWeb 中的静态资源访问
# Tomcat 中的两个默认 Servlet
Tomcat 有两个默认的 Servlet,你的 Web 项目会【无意中】用到它们。JSPServlet 和 DefaultServlet 。
JSPServlet 用于响应 .jsp
请求;DefaultServlet 则是默认的【兜底】的 Servlet 。
# JSPServlet
其实 JSPServlet 并非一个,应该是整个项目有多少个 JSP 页面,就会有对应的多少个 JSPServlet 。
JSPServlet 的工作大家都很清楚了,它涉及到 .jsp
文件的工作原理。
当你第一次访问一个 .jsp
页面时,Tomcat 会根据你的 jsp 页面【帮】你写一个 Servlet,即此处的 JSPServlet 。
访问 jsp 页面最终触发的是这个 Servlet 的执行。在这个 Servlet 中会生成一个页面的内容(html格式字符串),并发回给客户端浏览器。
# DefaultServlet
DefaultServlet 是 Tomcat 提供的默认的【兜底】的 Servlet,相当于它的 <urlpattern>
配置的是 /
。
DefaultServlet 中的 doPost 方法交由 doGet 方法进行处理。而 doGet 方法兜兜转转最后执行了一个 copy 方法,在 copy 方法中把找到静态资源文件,将其内容读出并写入 Response 对象的输出流,这样你的浏览器就看到静态数据了。
# 配置引起的 bug
结合我们自定义的 Servlet,和 JSPServlet、DefaultServlet,会让初学者造成一些不知所以的 bug :
# 情况一
将 HelloServlet 的 urlpattern 配置为 *.do
,此时项目中的各个 Servlet 的分工如下:
Tomcat 默认的 JSPServlet 负责响应
.jsp
请求。我们自己的 HelloServlet 负责响应
.do
请求。凡是没有 Servlet 响应的请求,都被【漏给】DefaultServlet 处理。
# 情况二
将 HelloServlet 的 urlpattern 配置为 /
,此时项目中的各个 Servlet 的分工如下:
Tomcat 默认 JSPServlet 负责响应
.jsp
请求。HelloServlet 负责响应所有的其它请求。
注意,你的 HelloServlet 就替代了 Tomcat 默认的 DefaultServlet 在做【兜底】的工作。
此时,你就无法访问静态资源!除非你的 HelloServlet 实现了 Tomcat 的 DefaultServlet 同样的功能。
# 情况三
将 HelloServlet 的 urlpattern 配置为 /*
,此时项目中的各个 Servlet 的分工如下:
所有的请求都由你的 HelloServlet 处理。
/*
是路径匹配,它的优先级高于 .jsp
。所以当用户输入 xxx.jsp
时,是 HelloServlet【先】响应了这个请求,轮不到 Tomcat 的 JSPServlet 来响应这个 .jsp
请求。
此时,在静态资源无法访问的基础上,jsp 也无法访问了。
# 总结
逻辑上,用户所访问的资源分为 3 种:
Servlet
JSP
静态资源(例如:html、css、js、png 等)
Tomcat 判断请求的资源的类型,也是按照上述顺序在判断:先判断是否是请求 Servlet(.do
请求),再判断是否是 JSP(.jsp
请求)。要是都不是,那么就是静态资源(.png
等请求)。
通过配置,进行合理安排,我们应该/预期达到如下效果:
对于 Servlet 的请求的处理,由我们自定义的
Servlet
进行处理。对于 JSP 的请求的处理,由 Tomcat 中的
JspServlet
自动处理。对于 静态资源 的处理,由 Tomcat 中的
DefaultServlet
自动处理。
注意
从本质上来讲,DefaultServlet 并不是『专门』处理静态资源的工具。而是说,既不是由我们自定义的 Servlet 处理的资源,又不是由 JspServlet 处理的资源,最后统统都交由 DefaultServlet 处理。
DefaultServlet 作为『兜底』的 Servlet ,它的 url-pattern 是 /
,注意,并非 /*
。
毫无疑问,web.xml
中 不需要 显示地配置 DefaultServlet(否则,它也就不会叫 Default Servlet 了)。
同样的道理,其实我们也从未在(也不需要在) web.xml
中显示地配置过 JspServlet 。
# 如何允许静态资源访问
当要访问静态资源时,可以在 web.xml
中明确指定什么样的请求(即对静态资源的请求)交由 DefaultServlet 进行处理(逻辑上,以下配置也可以省略不写,默认既是如此):
<servlet-mapping>
<servlet-name>default</servlet-name> <!-- 在默认的配置中,DefaultSevlet 的 servelt-name 就是叫 default -->
<url-pattern>*.html</url-pattern>
<url-pattern>*.css</url-pattern>
<url-pattern>*.js</url-pattern>
<url-pattern>*.jpg</url-pattern>
<url-pattern>*.png</url-pattern>
</servlet-mapping>
WARNING
WEB-INF
目录下内容不允许直接公共访问,所以静态资源通常是放到与WEB-INF
同级的目录下面。- 如果是 SpringMVC 项目,对于静态资源的访问有其他的操作。
# 2. 过滤器(Filter)
# 基本概念
过滤器(Filter)是拦截 Request 请求的对象:在用户的请求访问资源前处理 ServletRequest 和 ServletResponse 。
Filter 相关的接口有:Filter、FilterConfig、FilterChain 。
Filter 的实现必须实现 Filter 接口。这个接口包含了 Filter 的3个生命周期方法:init()
、doFilter()
、destroy()
。
Servlet 容器(Tomcat)初始化Filter时,会触发 Filter 的 init()
方法,一般来说是在应用开始时(注意,不是第一次使用时)。这个方法只会被调用一次。
FilterConfig 对象由 Servlet 容器传入 init()
方法中。
当 Servlet 容器每次处理 Filter 相关的资源时,都会调用该 Filter 实例的 doFilter()
方法。就像容器调用 Serviet 的 service()
方法。
在 Filter 的 doFilter()
方法中,最后一行需要调用 FilterChain 中的 doChain()
方法。注意,FilterChain 是 doFilter()
方法的第三个参数。
一个 URL 资源可能被多个 Filter 关联(即一条 Filter 链),这时 Filter.doFilter()
的方法将触发 Filter 链中下一个 Filter。只有在 Filter 链中最后一个 Filter 里调用 doFilter()
方法,才会触发 Controller 中处理 URL 资源的方法。
如果在 Filter 的 doFilter()
方法中,因为故意(或无意)没有调用 FilterChain 的 doFilter()
方法,那么这个 Request 请求将终止,后面的处理就会中断。
注意
,FilterChain 接口中,唯一的方法就是 doFilter()
方法,它和 Filter 接口中的 doFilter()
方法定义是不一样的。
Filter 接口中,最后一个方法是 destroy()
,该方法在 Servlet 容器要销毁 Filter 时触发。
类似于 Servlet,Filter 也是单例。
# Filter 的配置
和 Servlet 的配置非常相似,Filter 的配置主要有三方面:
- 确认哪些资源需要本 Filter 进行拦截处理。
- 配置 Filter 的初始化参数和值,这些参数在 Filter 的
init()
方法中可以读取到。- 给 Filter 取一个名称(一般来说这个配置是不需要的)。在一些特殊的情况下,系统通过这个名字来识别Filter。
@WebFilter(filterName = "firstFilter",
urlPatterns = {"/*"},
initParams = {
@WebInitParam(name="", value=""),
@WebInitParam(name="", value="")
})
public class FirstFilter implements Filter {
...
}
<filter>
<filter-name>firstFilter</filter-name>
<filter-class>com.hemiao.filter.FirstFilter</filter-class>
<init-param>
<param-name>author</param-name>
<param-value>ben</param-value>
</init-param>
<init-param>
<param-name>email</param-name>
<param-value>[email protected]</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>firstFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
# 3. 文件上传与下载
# 文件上传
为了能上传文件,必须将表单的 method
设置为 POST
,并将 enctype
设置为 multipart/form-data
。
有两种实现文件上传的方式:
底层使用 Apache Commons FileUpload 包
底层使用 Servlet 3.1 内置的文件上传功能
无论是哪种方式,其使用方式都是一样的,将 file 类型的请求参数绑定到请求处理方法的特定类型的参数上:
CommonsMultipartFile(commons-fileupload)
MultipartFile(Servlet 3.1)
# Web 3.0 的文件上传
普通的表单(form)元素无法直接上传文件,必须通过“特殊处理”。
对上传文件功能而言,有些特殊的地方:
- form 表单内,要添加控件
<input type="file" name="myfile">
- form 表单的提交方式必须是 post 方式
- form 表单的内容格式要定义成 multipart/form-data 格式
<form action="..." method="post" enctype="multipart/form-data">
...
</form>
enctype="multipart/form-data"
表示表单元素中的 input 数据以二进制的方式发送到服务端。此时如果是普通的 input 数据,无法像之前一样从 request 中直接获得。
对于上传文件的的大小问题,惯例是:
- 足够小的文件,先接收到内存中,最后写入磁盘。
- 稍大的文件,写入磁盘临时文件,最后写入最终目的地。
- 大型文件,禁止上传。
在 Web 3.0 之前 使用 commons-fileupload 库是最常见的上传办法。当 Servlet 的设计者意识到文件上传的重要性后,在 Web 3.0 中它就成了一项内置的特性。
Web 3.0 中的文件上传主要围绕着 MultipartConfig 注解和 Part 接口。
# @MultipartConfig 注解
- fileSizeThreshold 可选属性
- 超过该值大小的文件,在上传过程中,将被写入磁盘临时文件,而不是保存在内存中。
- maxFileSize 可选属性
- 每个上传文件的大小上限。
- maxRequestSize 可选属性
- 一次请求(可能包含多个上传)的大小上限。
@WebServlet(urlPatterns="/hello.do")
@MultipartConfig(maxFileSize = 5*1024*1024)
public class HelloServlet extends HttpServlet {
...
}
# Part 接口
在一个表单(Form)中,无论是否有文件上传控件,Servlet 3.0 都会将这些表单控件对应成代码中的一个 Part 对象。
通过 request 对象的 .getParts()
方法可以获得所有的这些 Part 对象。
Collection<Part> parts = request.getParts();
在一个或多个部分组成的请求中,每一个表单域(包括非文本域),都将被转换成一个 Part 。
普通文本域和文件上传域的区别在于,其 Part 对象的 .getContentType()
方法返回值的不同。对于普通文本域的 Part 对象而言,该方法返回 null 。
for (Part part : parts) {
if (part.getContentType() == null) {
System.out.println("普通文本域");
}
else {
System.out.println("文件上传域");
}
}
补充,如果是要获取普通文本域的值,其实直接使用正常 request.getParameter() 就行。
每一个 Part 分为“头”和“体”两部分。普通文本域只有头部,而文件上传域则有头有体。
普通文本域的头部形式为:
content-disposition:form-data; name="域名"
上传文本域的头部形式为:
content-type:内容类型
content-disposition:form-data; name="域名"; filename="文件名"
对我们而言,需要的是文本上传域中的 content-disposition 中的 filename 部分。
String header = part.getHeader("content-disposition");
// 内容为 form-data; name="域名"; filename="文件名"
通常会使用工具类,将 content-disposition 中的 filename 中的值截取出来。
private String getFileName(String header) {
String[] arr = header.split("; ");
String item = null;
for (String cur : arr) {
// System.out.println("debug: " + cur);
if (cur.startsWith("filename=")) {
item = cur;
break;
}
}
int start = item.indexOf('"')+1;
int end = item.lastIndexOf('"');
String filename = item.substring(start, end);
// System.out.println(filename);
return filename;
}
Part 对象直接提供了方法将上传文件的内容写入盘:
String savePath = request.getServletContext().getRealPath("/WEB-INF/uploadFile/");
String filePathName = savePath + File.separator + fileName; // 目标文件路径名
part.write(filePathName); // 把文件写到指定路径
Part的其它常用方法
getContentType()
方法- 获得Part的内容类型。如果Part是普通文本,那么返回null。
- 该方法可以用于区别是普通文本域,还是文件上传域。
getHeader()
方法- 该方法用于获取指定的标头的值。
- 对于上传文本域的 Part,该参数有
content-type
和content-disposition
- 对于普通文本域,则只有
content-disposition
一种。
getName()
方法- 无论是普通文本域Part,还是文件上传域Part,都是获得域名值。
write()
方法- 将上传文件写入磁盘中。
delete()
方法- 手动删除临时文件
getInputStream()
方法- 以InputStream形式返回上传文件的内容。
# 利用 commons-fileupload 文件上传
利用 commons-fileupload 文件上传需要利用引入 commons-fileupload 包(它依赖于 commons-io 包)
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.4</version>
</dependency>
作为 Servlet 内置上传功能之前的【准标准】,Servlet 在引入内置上传功能时借鉴了 commons-fileupload 的实现方式。因此,在了解 Servlet 内置上传功能之后,再回头看 commons-fileupload 文件上传时,你会发现它们的基本逻辑/大道理时一样的,只不过 commons-fileupload 的实现会罗嗦一些
在 Servlet 内置的上传功能中,从 request 中获得的是一个 Part[]
,其中的每一个 Part 对象对应着表单中的一个表单域(Form Field)。而 commons-fileupload 中我们从 request 中获得的是一个 List<FileItem>
,commons-fileupload 中使用 FileItem 来对应每一个表单域,起到和 Part 一样的作用。
commons-fileupload 的罗嗦体现在以下几个方面:
- commons-fileupload 不能对 Servlet 使用注解,因此相关的上传配置需要通过编码实现。
- commons-fileupload 不能使用
request.getParameter()
为了能够从 request 中获得 List<FileItem>
,你需要两个对象:
// 创建上传所需要的两个对象
DiskFileItemFactory factory = new DiskFileItemFactory(); // 磁盘文件对象
ServletFileUpload sfu = new ServletFileUpload(factory); // 文件上传对象
如果不做出设置,那么相关设置则采用默认值。
// 设置上传过程中所收到的数据是【存内存】还是【存磁盘】的阈值
factory.setSizeThreshold(100 * 1024);
// 设置磁盘临时文件的保存目录
factory.setRepository(new File("D:/upload"));
// 设置解析文件上传中的文件名的编码格式,解决上传文件名中文乱码问题
sfu.setHeaderEncoding("utf-8");
// 限制单个文件的大小
sfu.setFileSizeMax(10*1024);
// 限制上传的总文件大小
sfu.setSizeMax(100*1024);
在创建文件上传对象(并作出相应设置)之后,我们可以通过它从 request 中获取我们所需要的 List<FileItem>
。
List<FileItem> list = sfu.parseRequest(request);
FileItem 自带了方法,可以判断当前的 FileItem 对应的是页面上的普通文本域,还是文件上传域:
for (FileItem item : list) {
if (item.isFormField()) {
System.out.println("普通文本域");
}
else {
System.out.println("文件上传域");
}
}
由于 commons-fileupload 中无法使用 request.getParameter()
,因此,为了获得普通文本域中的数据,需要使用 FileItem 自己的方法:
for (FileItem item : list) {
if (item.isFormField()) {
String fieldName = item.getFieldName(); // 例如:username / password
String fieldValue = item.getString("UTF-8"); // 例如,tom / 123456
System.out.println(fieldName + ": " + fieldValue);
}
else {
System.out.println("文件上传域");
}
}
由于 commons-fileupload 引用了 commons-io,所以,将上传的文件内容写入磁盘倒是十分简单:
for (FileItem item : list) {
if (item.isFormField()) {
...
}
else {
System.out.println("文件上传域");
// 创建输出文件
String name = item.getName();// 获取上传文件的名字
String outPath = "D:/upload/" + name;
FileOutputStream output = new FileOutputStream(new File(outPath));
// 获得上传文件字节流
InputStream input = item.getInputStream();
// 使用 IOUtils 工具将输入流中的数据写入到输出流。
IOUtils.copy(input, output);
System.out.println("上传成功!保存的路径为:" + outPath);
input.close(); // 关闭输入流
output.close(); // 关闭输出流
item.delete(); // 删除处理文件上传时生成的临时文件
}
}
# 文件下载
内容类型 | 文件扩展名 | 描述 |
---|---|---|
text/plain | txt | 文本文件(包括但不仅包括txt) |
application/msword | doc | Microsoft Word |
application/pdf | Adobe Acrobat | |
application/zip | zip | winzip |
audio/mpeg | mp3 | mp3 音频文件 |
image/gif | gif | COMPUSERVE GIF 图像 |
image/jpeg | jpeg jpg | JPEG 图像 |
image/png | png | PNG 图像 |
详细 MIME 参见 网址 (opens new window) 。
相对于上传而言,下载文件较为简单,只需要完成两步:
- 设置响应的内容类型。
- 添加一个
content-disposition
到响应标头(addHeader()
方法),其值为:attachment; filename=文件名
- 通过 resp 对象获得输出流,将文件内容发送至客户端。
resp.setContentType("text/plain"); // step 1
resp.setHeader("content-disposition", "attachment;filename=" + URLEncoder.encode("D:/note.txt", "UTF-8")); // step 2
InputStream is = new FileInputStream(new File("D:/note.txt"));
OutputStream out = resp.getOutputStream();
byte[] buffer = new byte[1024];
int n = 0;
while ((n = is.read(buffer)) > 0) {
out.write(buffer, 0, n); // step 3
}
is.close();
out.close();
System.out.println("下载成功");
# 4. Cookies
Cookie 本质上是一个小文件,由浏览器创建/管理,保存在浏览器本地,即用户自己的电脑上。
当你访问一个网站/网址时,浏览器会“帮”你将这个文件的内容发送至服务端(Tomcat)。这个小文件的内容都是“名值(name-value)对”。只有浏览器本地有这个网站/网址的相关 Cookie(小文件),浏览器『一定』会把它的内容“帮”你发送到服务端。这个过程无需程序员参与,不受程序员的控制。
浏览器“帮”你发送的 Cookie,可能不止一个。服务端获得浏览器发送来的所有 Cookie 的方法是通过 request 对象的 getCookies()
。
Cookie(小文件)是由浏览器在本地创建,但是,它是由服务端“通知/要求”浏览器去创建时,才会创建的。
浏览器通常支持每个网站 20 个 cookies 。
注意
,cookie 中不能直接存放中文,所以需要做相应的处理。常见处理办法是使用 URLEncoder 和 URLDecoder 将中文字符串编码/解码成URL格式编码字符串。
可以通过传递 name 和 value 两个参数给Cookie类的构造函数来创建一个 cookie。在创建完 cookie 之后,可以设置它的 maxAge
属性,这个属性决定了cookie 何时过期(单位为秒)。
要将 cookie 发送到浏览器,需要调用 HttpServletResponse 的 add()
方法。
服务器若要读取浏览器提交的 cookie,可以通过 HttpServletRequest 接口的 getCookie()
方法,该方法返回一个 Cookie 数组,若没有 cookies 则返回 null 。你需要遍历整个数组来查询某个特定名称的 cookie 。
注意,并没有一个直接的方法来删除一个 cookie,你只能创建一个同名的 cookie,并将 maxAge
设置为 0,并添加到 HttpServletResponse 中来“覆盖”原来的那个 cookie 。
Cookie 最大的问题在于用户可以通过设置禁用浏览器的 cookie 功能。
# 5. 监听器(Listener)
为了能够在 Servlet/JSP 应用程序中进行事件驱动编程(Event-Driven Programming),Servlet API 提供了一整套事件类和监听器接口。所以事件类均源自 java.util.Event,并且监听器在以下三个不同级别中均可使用:ServeletContext、HttpSession 及 ServletRequest 。
# 监听器接口和注册
创建监听器时,只要创建一个实现相关接口的 Java 类即可。在 Servlet 3.0 中,监听器有 2 种方法,以便 Servlet 容器能够认出来。
使用 @WebListener 注解
@WebListener public class 自定义监听器 implements 监听器接口 { }
在部署描述符(web.xml)使用一个 <listener> 元素:
<listener> <listener-class>自定义监听器</listener-class> </listener>
在应用程序中可以想要多少个监听器就可以有多少个监听器。注意,对监听器方法的调用时同步进行的。
# Servelt Context 监听器
在 ServletContext 级别上有 2 个监听器接口:
- ServletContextListener
- ServletContextAttributeListener
# ServletContextListener
ServletContextListener 会对 ServletCotnext 的初始化(init)和解构(destroy)做出响应。ServletContext 被初始化时,Servlet 容器会在所有已注册的 ServletContextListner 中调用 contextInitialized 方法。
void contextDestroyed(ServletContextEvent event);
当 ServletContext 要被解构和销毁时,Servlet 容器会在所有已注册的 ServletContextListener 中调用 contextDestroyed 方法。
void contextDestroyed(ServletContextEvent event);
contextInitialized 和 contextDestroyed 都会收到一个来自 Servlet 容器的 ServletContextEvent 。ServletContextEvent 是 java.util.EventObject 的子类,它定义了一个 getServletContext 方法,用以返回 ServletContext :
ServletContext getServletContext();
# ServletContextAttributeListener
每当 ServletContext 中添加、删除或替换了某个属性时,ServletContextAttributeListener 的事件都会收到通知。以下就是在这个监听器接口中定义的三个方法:
void attributeAdded(ServletContextAttributeEvent event);
void attributeRemoved(ServletContextAttributeEvent event);
void attributeReplaced(ServletContextAttributeEvent event);
- 每当 ServletContext 中添加了某个属性时,Servlet 容器就会调用 attributeAdded 方法;
- 每当 ServletContext 中移除了某个属性时,Servlet 容器就会调用 attributeRemoved 方法;
- 每当 ServletContext 被新的代替时,Servlet 容器就会调用 attributeReplaced 方法。
所有的监听器方法都会收到一个 ServletContextAttributeEvent 实例,从这个参数中你可以获取属性名称(getName)和属性值(getValue)。
# Session 监听器
与 HttpSession 有关的监听器有 4 个,我们常见的 2 个是:HttpSessionListener 和 HttpSessionAttributeListener 。
# HttpSessionListener
当有 HttpSession 被创建或销毁时,Servlet 容器就会调用所有已注册的 HttpSessionListener。HttpSessionListener 中定义的 2 个方法是:
void sessionCreated(HttpSessionEvent event);
void sessionDestroyed(HttpSessionEvent event);
这 2 个方法都会收到一个 HttpSessionEvent 实例,我们可以调用这个参数的 getSession 方法来获得所创建或销毁的 HttpSession 对象。
# HttpSessionAttributeListener
HttpSessionAttributeListener 就像 ServletContextAttributeListener 一样,只不过当 HttpSession 中有添加、删除或替换属性的时候它才会调用。它定义的方法有:
void attributeAdded(HttpSessionBindingEvent event);
void attributeRemoved(HttpSessionBindingEvent event);
void attributeReplaced(HttpSessionBindingEvent event);
- 当 HttpSession 中添加某个属性时,由 Servlet 容器调用 attributeAdded 方法;
- 当 HttpSession 中删除某个属性时,由 Servlet 容器调用 attributeRemoved 方法;
- 当 HttpSession 属性被新属性代替时,由 Servlet 容器调用 attributeReplaced 方法。
所有的监听器方法都会收到一个 HttpSessionBindingEvent 实例,从这个参数中,你可以获得响应的 HttpSession 对象和属性名以及属性值。
# ServletRequest 监听器
在 ServletRequest 级别上有 3 个监听器接口,我们常见的有 2 个:ServletRequestListener 和 ServletRequestAttributeListener 。
# ServletRequestListener
ServletRequestListener 对 ServletRequest 的创建和销毁做出响应。在 Servlet 容器中时通过池(pool)来重用 ServletRequest 的,“创建” ServletRequest 花费的事件相当于从池中获取一个 ServletRequest 对象的事件,销毁它的时间则相当于将 ServletRequest 重新放回 pool 的时间开销。
ServletRequestListener 接口定义了 2 个方法:
void requestInitialized(ServletRequestEvent event);
void requestDestroyed(ServletRequestEvent event);
创建(本质上是从池中取出)ServletRequest 时会调用 requestInitialized 方法,ServletRequest 被销毁(本质上是放回池中)时会调用 requestDestroyed 方法。这 2 个方法都会收到一个 ServletRequestEvent,通过调用 event 的 getServletRequest 方法,可以从中获取到相应的 ServletRequest 实例
ServletRequest getServletRequest()
另外,ServletRequestEvent 接口还定义了返回 ServletContext 的 getServletContext 方法,方法签名如下:
ServletContext getServletContext()
# ServletRequestAttributeListener
当在 ServletRequest 中添加、删除或者替换某个属性时,会调用 ServletRequestAttributeListener 。ServletRequestAttributeListener 接口中定义了 3 个方法:
void attributeAdded(ServletRequestAttributeEvent event)
void attributeRemoved(ServletRequestAttributeEvent event)
void attributeReplaced(ServletRequestAttributeEvent event)
所有方法都会收到一个 ServletRequestAttributeEvent 实例。通过参数 event 的 getName 和 getValue 方法你可以获得属性名和属性值。