过滤器和监听器是Servlet规范里的两个高级特性, 过滤器的作用是通过对request、response 的修改实现特定的功能,例如请求数据字符编码、IP地址过滤、异常过滤、用户身份认证等。监听器的作用是用于监听Web程序中正在执行的程序, 根据发生的事件做出特定的响应。合理利用这两个特性, 能够轻松解决某些Web特殊问题。
Servlet进阶API
在编写完一个Servlet类后, 通常需要在web.xml中或者通过注解进行相关的配置, 这样Web 容器才能读取Servlet设置的信息, 包括其类地址、初始化等。对于每个Servlet的配置, Web都会生成与之相对应的ServletConfig对象, 从ServletConfig对象中可以得到Servlet的初始化参数。
Servlet、ServletConfig与Generic Servlet
本节将介绍ServletConfig与GenericServlet 的关系,以及如何使用ServletConfig 和ServletContext对象来获取Servlet初始化参数。
在Web容器启动后, 通过加载web.xml文件读取Servlet的配置信息, 实例化Servlet类, 并且为每个Servlet配置信息产生唯一一个ServletConfig对象。在运行Servlet时, 调用Servlet接口的init() 方法, 将产生的ServletConfig作为参数传入Servlet中, 流程如图所示。
正如前面所介绍的, 初始化方法只会被调用一次, 即容器在启动时实例化Servlet 和创建ServletConfig对象, 且Servlet与ServletConfig是一一对应关系, 之后就直接执行service() 方法。
从Java ServletAPI中可以得知GenericServlet类同时实现了Servlet、ServletConfig两个接口。
ServletConfig接口
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package javax.servlet;
import java.util.Enumeration;
public interface ServletConfig {
String getServletName();
ServletContext getServletContext();
String getInitParameter(String var1);
Enumeration<String> getInitParameterNames();
}
Servlet接口
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package javax.servlet;
import java.io.IOException;
public interface Servlet {
void init(ServletConfig var1) throws ServletException;
ServletConfig getServletConfig();
void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException;
String getServletInfo();
void destroy();
}
GenericServlet.java
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package javax.servlet;
import java.io.IOException;
import java.io.Serializable;
import java.util.Enumeration;
public abstract class GenericServlet implements Servlet, ServletConfig, Serializable {
private static final long serialVersionUID = 1L;
private transient ServletConfig config;
//无参构造函数
public GenericServlet() {
}
//销毁方法
public void destroy() {
}
//获取初始化参数
public String getInitParameter(String name) {
return this.getServletConfig().getInitParameter(name);
}
//获取初始化参数名称
public Enumeration<String> getInitParameterNames() {
return this.getServletConfig().getInitParameterNames();
}
//获取servlet配置
public ServletConfig getServletConfig() {
return this.config;
}
//获取servlet上下文
public ServletContext getServletContext() {
return this.getServletConfig().getServletContext();
}
//获取servlet信息
public String getServletInfo() {
return "";
}
//初始化
public void init(ServletConfig config) throws ServletException {
this.config = config;
this.init();
}
//初始化
public void init() throws ServletException {
}
//日志
public void log(String message) {
this.getServletContext().log(this.getServletName() + ": " + message);
}
//日志
public void log(String message, Throwable t) {
this.getServletContext().log(this.getServletName() + ": " + message, t);
}
//抽象service方法
public abstract void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException;
//获取servlet名称
public String getServletName() {
return this.config.getServletName();
}
}
这个类的存在使得编写Servlet更加方便, 它提供了一个简单的方法, 这个方法用来执行有关Servlet生命周期的方法以及在初始化时对ServletConfig对象和ServletContext对象进行说明。
分析上述代码中的getServletName和init方法, 得知GenericServlet类将ServletConfig封装了。
从代码getServletConfig可知, GenericServlet类还定义了获取ServletConfig对象的方法, 当编写Servlet类时就可以通过这些方法来获取所要配置的信息, 而不用重新创建出ServletConfig对象。
使用ServletConfig
前面已经介绍过, 当容器初始化Servlet时, 会为Servlet创建唯一的ServletConfig对象。利用Web容器读取web.xml文件, 将初始化参数传给ServletConfig,而ServletConfig作为对象参数传递到init() 方法中。
public interface ServletConfig {
//该方法返回一个Servlet实例的名称
String getServletName();
//返回一个ServletContext对象的引用
ServletContext getServletContext();
//返回一个由参数String name决定的初始化变量的值, 如果该变量不存在, 则返回null
String getInitParameter(String var1);
//返回一个存储所有初始化变量的枚举类型。如果Servlet没有初始化变量,则返回一个空枚举类型
Enumeration<String> getInitParameterNames();
}
从Servlet 3.0开始, 允许以注入的方式配置Servlet,而不仅仅在web.xml中配置。因此配置Servlet的形式可以有如下形式:
@WebServlet(
name = "servletConfigDemo",
urlPatterns = {
"/servletconfig"
},
loadOnStartup = 1,
displayName = "demo",
initParams = {
@WebInitParam(name="success",value = "success.html"),
@WebInitParam(name="error",value = "error.html")
}
)
等价xml配置
<servlet>
<display-name>demo</display-name>
<servlet-name>servletConfigDemo</servlet-name>
<servlet-class>com.wujialiang.HellServlet</servlet-class>
<load-on-startup>1</load-on-startup>
<init-param>
<param-name>success</param-name>
<param-value>success.html</param-value>
</init-param>
<init-param>
<param-name>error</param-name>
<param-value>error.html</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>servletConfigDemo</servlet-name>
<url-pattern>/servletconfig</url-pattern>
</servlet-mapping>
上述的两个配置过程是等价的, 在Servlet 3.0及4.0中可以同时兼容, 在Servlet 2.0中只能在web.xml中配置。@WebServlet和@WebInitParam的主要属性分别参见下表。
@WebServlet的主要属性列表
属性名 | 描述 |
---|---|
name | 指定Servlet的name 属性, 等价于≷servlet-name>, 如果没有指定, 则该Servlet的取值即为类的全名 |
value | 该属性与url Patterns属性等价, 但两个属性不能同时使用 |
urlPatterns | 指定Servlet的URL匹配模式, 等价于≷url-pattern>标签 |
loadOnStartup | 指定Servlet的加载顺序, 等价于标签。当值为0或者大于0时, 表示容器在应用启动时就加载并初始化这个Servlet; 当值小于0或者没有指定时, 则表示容器在该Servlet被选择时才会去加载; 正数的值越小, 该Servlet的优先级越高, 应用启动时就越先加载;当值相同时,容器就会自己选择顺序来加载 |
initParams | 指定Servlet的初始化参数, 等价于≷init-param>标签 |
asyncSupported | 声明Servlet是否支持异步操作模式, 等价于标签, 该属性在Servlet 3.0中才有 |
@WebInitParam的主要属性列表
属性名 | 描述 |
---|---|
name | 指定参数的名字, 等价于≷param-name> |
value | 指定参数的值, 等价于≷param-value> |
description | 参数的描述, 等价于≷description> |
利用初始化信息设定跳转信息
新建index.jsp
<%@page language="java" import="java.util.*" pageEncoding="utf-8" %>
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<%
String userId = (String)session.getAttribute("user");
%>
你好,<%=userId%>
</body>
</html>
新建error.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<h1>
登录失败
</h1>
新建ServletConfigDemo.java
package com.wujialiang;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebInitParam;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.Enumeration;
@WebServlet(
name = "servletConfigDemo",
urlPatterns = {
"/servletconfig"
},
loadOnStartup = 1,
displayName = "demo",
initParams = {
@WebInitParam(name = "success", value = "index.jsp"),
@WebInitParam(name = "error", value = "error.jsp")
}
)
public class ServletConfigDemo extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doPost(req,resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
ServletConfig servletConfig = getServletConfig();
//获取参数
String success = servletConfig.getInitParameter("success");
String error = servletConfig.getInitParameter("error");
System.out.println("success:" + success);
System.out.println("error:" + error);
//获取全部参数
Enumeration<String> initParameterNames = servletConfig.getInitParameterNames();
while (initParameterNames.hasMoreElements()) {
String name = (String) initParameterNames.nextElement();
String value = servletConfig.getInitParameter(name);
System.out.println(name + ":" + value);
}
//获取servlet上下文
ServletContext servletContext = servletConfig.getServletContext();
System.out.println("servletContext:" + servletContext);
//获取servlet名字
String servletName = servletConfig.getServletName();
System.out.println("servletName" + servletName);
req.setCharacterEncoding("utf-8");
resp.setContentType("text/html;charset=utf-8");
String userId = req.getParameter("userid");
String pwd = req.getParameter("pwd");
if(userId.equals("admin")&&pwd.equals("123456")){
HttpSession session = req.getSession();
session.setAttribute("user",userId);
RequestDispatcher requestDispatcher = req.getRequestDispatcher(success);
requestDispatcher.forward(req,resp);
}else{
RequestDispatcher requestDispatcher = req.getRequestDispatcher(error);
requestDispatcher.forward(req,resp);
}
}
}
访问http://localhost:8080/web09_war/servletconfig?userid=admin&pwd=123456
访问http://localhost:8080/web09_war/servletconfig?userid=admin&pwd=1234567
使用ServletContext
ServletContext对象是Servlet中的全局存储信息, 当服务器启动时, Web容器为Web应用创建唯一的ServletContext 对象, 应用内的Servlet 共享同一个ServletContext。可以认为在ServletContext中存放着共享数据, 应用内的Servlet可以通过ServletContext对象提供的方法获取共享数据。ServletContext对象只有在Web应用被关闭的时候才销毁。
ServletContext接口中定义了运行Servlet应用程序的环境信息, 可以用来获取请求资源的URL、设置与存储全局属性、Web应用程序初始化参数。ServletContext中的常见方法如表所示。
方法 | 描述 |
---|---|
getRealPath(String path) | 获取给定的虚拟路径所对应的真实路径名 |
getResource(String uri path) | 返回由path指定的资源路径对应的一个URL对象 |
getResourceAsStream(String uri path) | 返回一个指定位置资源的InputStream。返回的InputStream可以是任意类型和长度的。使用时指定路径必须以“/”开头,表示相对于应用程序环境根目录 |
getRequestDispatcher(String uri path) | 返回一个特定URL的RequestDispatcher对象, 否则就返回一个空值 |
getResourcePaths(String path) | 返回一个存储web-app中所指资源路径的Set(集合) , 如果是一个目录信息,会以“/”作为结尾 |
getServerInfo | 获取服务器的名字和版本号 |
package com.wujialiang;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.annotation.WebInitParam;
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.io.InputStream;
import java.util.Set;
@WebServlet(
name = "servletcontext",
urlPatterns = {
"/servletcontext"
},
loadOnStartup = 0,
displayName = "context",
initParams = {
@WebInitParam(name="dir",value = "/"),
@WebInitParam(name = "success",value = "index.jsp"),
@WebInitParam(name = "resourcePath",value = "test.txt"),
}
)
public class ServletContextDemo extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html;charset=utf-8");
String dir = getInitParameter("dir");
String success = getInitParameter("success");
String resourcePath = getInitParameter("resourcePath");
ServletContext servletContext = getServletContext();
//获取真实路径
String realPath = servletContext.getRealPath(success);
System.out.println("index.jsp真实路径为:"+realPath);
Set<String> sets = servletContext.getResourcePaths(dir);
for (Object set:sets
) {
System.out.println("文件内容:"+(String)set);
}
String serveInfo =servletContext.getServerInfo();
System.out.println("服务器版本为:"+serveInfo);
//获取资源文件内容
InputStream resourceAsStream = servletContext.getClassLoader().getResourceAsStream(resourcePath);
ServletOutputStream outputStream = resp.getOutputStream();
byte[] buffer = new byte[1024];
while (resourceAsStream.read(buffer)!=-1){
outputStream.write(buffer);
}
resourceAsStream.close();
outputStream.close();
}
}
以“/”作为开头时称为环境相对路径。在Servlet中, 若是环境相对路径, 则直接委托给ServletContext的get RequestDispatcher() 。
应用程序事件、监听器
何谓监听?顾名思义就是监视行为。在Web系统中, 所谓的监听器就是应用监听事件来监听请求中的行为而创建的一组类。HttpServletRequest、HttpSession、ServletContext对象在Web容器中遵循生成、运行、销毁这样的生命周期。当进行相关的监听配置后, Web容器就会调用监听器上的方法,进行对应的事件处理,从而了解运行的情况或者运行其他的程序。各监听器接口和事件类如表所示。
与ServletContext相关
监听接口 | 监听事件 |
---|---|
ServletContextListener | ServletContextEvent |
ServletContextAttributeListener | ServletContextAttributeEvent |
HttpSession相关
监听接口 | 监听事件 |
---|---|
HttpSessionIdListener | HttpSessionEven |
HttpSessionListener | HttpSessionEven |
HttpSessionActivationListener | HttpSessionEven |
HttpSessionAttributeListener | HttpSessionBindingEvent |
HttpSessionBindingListener | HttpSessionBindingEvent |
ServletRequest相关
监听接口 | 监听事件 |
---|---|
ServletRequestListener | ServletRequestEvent |
ServletRequestAttributeListener | ServletRequestAttributeEvent |
使用监听器需要实现相应的监听接口。在触发监听事件时,应用服务器会自动调用监听方法。开发人员不需要关心应用服务器如何调用,只需要实现这些方法就行。
ServletContext事件监听器
与ServletContext 有关的监听器有两个,即ServletContextListener 与ServletContextAttributeListener。
ServletContextListener
ServletContext Listener被称为“ServletContext生命周期监听器”,可以用来监听Web程序初始化或者结束时响应的动作事件。ServletContext Listener接口的类是javax.servlet.ServletContext Listener,该接口提供两个监听方法。
default void contextInitialized(ServletContextEvents ce)
:该方法用于通知监听器,已经加载Web应用和初始化参数。default void contextDestroyed(ServletContextEvents ce)
:该方法用于通知监听器, Web 应用即将关闭。
在Web应用程序启动时, 会自动开始监听, 首先调用的是contextInitialized() 方法, 并传入ServletContextEvent 参数, 它封装了ServletContext 对象, 可以通过ServletContextEvent的getServletContext() 方法取得ServletContext对象, 通过getInitParameter() 方法取得初始化参数。在Web应用关闭时, 会自动调用contextDestroyed() 方法, 同样会传入ServletContextEvent参数。在contextInitialized() 中可以实现应用程序资源的准备事件, 在contextDestroyed() 中可以实现对资源的释放。例如, 可以在contextInitialized() 方法中实现Web应用的数据库连接、读取应用程序设置等;在contextDestroyed() 中设置数据库资源的释放。
在Web中, 实现ServletContext Listener的步骤如下:
- 首先, 编写一个监听类并实现ServletContext Listener接口
- 进行相关的配置:
<listener>
<listener-class>com.wujialiang.MyServletListener</listener-class>
</listener>
- 或者利用注入的方式注入监听类:
@WebListener
public class MyServletContextListener implements ServletContextListener{
}
- 若需要初始化参数, 则需要在web.xml中进行配置, 例如:
<context-param>
<param-name>user_name</param-name>
<param-value>wjl</param-value>
</context-param>
@WebListener也是Servlet 3.0才有的, 因为它没有设置初始化参数的属性, 所以也需要在web.xml中设定。
web.xml
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
<display-name>WebApp</display-name>
<context-param>
<param-name>user_name</param-name>
<param-value>wujialiang</param-value>
</context-param>
</web-app>
新建MyServletContextListener.java
package com.wujialiang;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
@WebListener
public class MyServletContextListener implements ServletContextListener {
public void contextInitialized(ServletContextEvent sce) {
//获取ServletContext
ServletContext servletContext = sce.getServletContext();
String user_name = servletContext.getInitParameter("user_name");
System.out.println("获取到user_name的值:" + user_name);
System.out.println("Tomcat启动中...");
}
public void contextDestroyed(ServletContextEvent sce) {
System.out.println("Tomcat正在关闭...");
}
}
启动tomcat
关闭tomcat
ServletContextAttributeListener
ServletContextAttributeListener被称为“ServletContext属性监听器”,可以用来监听Application
属性的添加、移除或者替换时响应的动作事件。
ServletContextAttributeListener接口的类是javax.servlet.ServletContextAttributeListener,该接口
提供3个监听方法。
default void attributeAdded(ServletContextAttributeEvent scab)
:该方法用于通知监听器,有对象或者属性被添加到Application中。default void attributeRemoved(ServletContextAttributeEvent scab)
:该方法用于通知监听器,有对象或者属性被移除到Application中。default void attributeReplaced(ServletContextAttributeEvent scab)
:该方法用于通知监听器,有对象或者属性被更改到Application中。
在ServletContext中添加属性、移除属性或者更改属性时,与其相对应的方法就会被调用。同样,在Web应用程序中,实现ServletContextAttributeListener的方法也有两种,形式如下。
- 利用注入的方式注入监听类:
@WebListener
public class MyServletContextAttributeListener implements ServletContextAttributeListener{
}
在web.xml中配置
<listener>
<listener-class>
com.wujialiang.MyServletContextAttributeListener
</listener-class>
</listener>
HttpSession事件监听器
从刚开始的表中可以发现,与HttpSession有关的监听器有5个:HttpSessionldListener、HttpSessionListener、HttpSessionAttributeListener HttpSessionBindingListener HttpSessionActivationListener.Servlet 3.1版本开始,增加了HttpSessionldListener。.
HttpSessionldListener
HttpSessionIdListener用来监听session ID的变化。
HttpSessionldListener接口的类是javax.servlet…http.HttpSessionldListener,该接口只提供了一个监听方法。
public void sessionldChanged(HttpSessionEvent se,java.lang.String oldSessionld)
:该方法用于通知监听器session ID发生了改变。
请求的session ID发生变化时,会触发sessionlDChanged()方法,并传入HttpSessionEvent和oldSessionld参数,可以使用HttpSessionEvent中的getSession()-getIdO获取新的session ID,oldSessionld代表改变之前的session ID。
在Web应用程序中,实现HttpSessionldListener的方法同样有两种,形式如下。
- 利用注入的方式注入监听类:
@WebListener
public class MyHttpSessionldListener implements HttpSessionIdListener{
}
- 在web.xml中配置:
<listener>
<listener-class>
com.wujialiang.MyHttpSessionldListener
</listener-class>
</listener>
HttpSessionListener
HttpSessionListener是“HttpSession生命周期监听器”,可以用来监听HttpSession对象初始化或者结束时响应的动作事件。
HttpSessionListener接口的类是javax.servlet…http.HttpSessionListener,该接口提供两个监听方法。
default void sessionCreated(HttpSessionEvent se)
:该方法用于通知监听器,产生了新的会话。
default void sessionDestroyed(HttpSessionEvent se)
:该方法用于通知监听器,已经消除一个会话。
在HttpSession对象初始化或者结束前,会自动调用sessionCreated(方法和sessionDestroyed()方法,并传入HttpSessionEvent参数,它封装了HttpSession对象,可以通过HttpSessionEvent的getSession()方法取得HttpSession对象。在Web应用程序中,实现HttpSessionListener的方法同样有两种,形式如下。
- 注入
@WebListener
public class MyHttpSessionListener implements HttpSessionListener{
}
- web.xml
<listener>
<listener-class>
com.wujialiang.MyHttpSessionListener
</listener-class>
</listener>
新建MyHttpSessionListener.java
package com.wujialiang;
import javax.servlet.annotation.WebListener;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
@WebListener
public class MyHttpSessionListener implements HttpSessionListener {
private static int count=0;
public static int getCount(){
return count;
}
public void sessionCreated(HttpSessionEvent se) {
MyHttpSessionListener.count++;
System.out.println("session增加");
}
public void sessionDestroyed(HttpSessionEvent se) {
MyHttpSessionListener.count--;
System.out.println("session减少");
}
}
新建LoginServlet.java
package com.wujialiang;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebInitParam;
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.HashMap;
import java.util.Map;
@WebServlet(name = "login",
loadOnStartup = 1,
urlPatterns = {
"/login",
},initParams = {
@WebInitParam(name = "success",value = "success.jsp")
})
public class LoginServlet extends HttpServlet {
Map<String,String> users;
public LoginServlet(){
users = new HashMap<>();
users.put("wjl001","123456");
users.put("wjl002","123456");
users.put("wjl003","123456");
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doPost(req,resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setCharacterEncoding("utf-8");
System.out.println("进入LoginServlet");
String userId = req.getParameter("userid");
String pwd = req.getParameter("pwd");
if(users.containsKey(userId)&&users.get(userId).equals(pwd)){
req.getSession().setAttribute("user",userId);
req.getSession().setAttribute("count",MyHttpSessionListener.getCount());
}
String success =getInitParameter("success");
resp.sendRedirect(success);
}
}
新建success.jsp
<%@ page import="java.util.*" contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>登录成功</title>
</head>
<body>
<h3>目前在线人数:<%=session.getAttribute("count")%></h3>
<p>欢迎您:<%=session.getAttribute("user")%></p>
</body>
</html>
访问http://localhost:8080/web01_war/login?userid=wjl001&pwd=123456
HttpSessionAttributeListener
HttpSessionAttributeListener是“HttpSession属性改变监听器”,可以用来监听HttpSession对象加入属性、移除属性或者替换属性时响应的动作事件。
HttpSessionAttributeListener接口的类是javax.servlet…htp.HttpSessionAttributeListener,.该接口提供3个监听方法。
default void attributeAdded(HttpSessionBindingEvent se)
:该方法用于通知监听器,已经在Session中添加一个对象或者变量。default void attributeRemoved(HttpSessionBindingEvent se)
:该方法用于通知监听器,已经在Session中移除一个对象或者变量。default void attributeReplaced(HttpSessionBindingEvent se)
:该方法用于通知监听器,已经在Session中替换一个对象或者变量。
当对session范围的对象或者变量进行操作时,Web容器会自动调用与实现接口类相对应的方法。HttpSessionBindingEvent是一个对象,可以利用其getName()方法得到操作对象或者变量的名称,利用getValue()方法得到操作对象或者变量的值。
在Web应用程序中,实现HttpSessionAttributeListener的方法同样有两种,形式如下。
- 注入方式
@WebListener
public class MyHttpSessionAttributeListener implements HttpSessionAttributeListener{
}
- web.xml中配置
<listener>
<listener-class>
com.wujialiang.MyHttpSessionAttributeListener
</listener-class>
</listener>
HttpSessionBindingListener
HttpSessionBindingListener是“HttpSession对象绑定监听器”,可以用来监听HttpSession中设置成HttpSession属性或者从HttpSession中移除时得到session的通知。
HttpSessionBindingListener接口的类是javax.servlet.http.HttpSessionBindingListener,该接口提供两个监听方法。
default void valueBound(HttpSessionBindingEvent event)
:该方法用于通知监听器,已经绑定一个session范围的对象或者变量。default void valueUnbound(HttpSessionBindingEvent event)
:该方法用于通知监听器,已经解绑一个session范围的对象或者变量。
参数HttpSessionBindingEvent是一个对象,可以通过getSession()方法得到当前用户的session,通过getName()方法得到操作的对象或者变量名称,通过getValue()方法得到操作的对象或者变量值。
在Web应用程序中实现HttpSessionBindingListener接口时,不需要注入或者在web.xml中配置,只需将设置成session范围的属性实现HttpSessionBindingListener接口就行。
HttpSessionActivationListener
HttpSessionActivationListener是“HttpSession对象转移监听器”,可以用来实现它对同一会话在不同的JVM中转移,例如,在负载均衡中,Wb的集群服务器中的JVM位于网络中的多台机器中。当session要从一个JVM转移至另一个JVM时,必须先在原来的JVM上序列化所有的属性对象,若属性对象实现HttpSessionActivationListener,就调用sessionWillPassivate()方法,而转移后,就会调用sessionDidActivate(O方法。
HttpSessionActivationListener接口的类是javax.servlet…http.HttpSessionActivationListener,该接口提供两个监听方法。
default void sessionDidActivate(HttpSessionEvent se)
:该方法用于通知监听器,该会话已变为有效状态。default void session WillPassivate(HttpSessionEvent se)
:该方法用于通知监听器,该会话已变为无效状态。
HttpServletRequest事件监听器
与HttpServletRequest有关的监听器有两个:ServletRequestListener、ServletRequestAttributeListener.
ServletRequestListener
ServletRequestListener是“Request生命周期监听器”,可以用来监听Reuqest对象初始化或者结束时响应的动作事件。
ServletRequestListener接口的类是javax…servlet…ServletRequestListener,该接口提供两个监听方法。
default void requestInitialized(ServletRequestEvent sre)
:该方法用于通知监听器,产生了新的request对象。default void requestDestroyed(ServletRequestEvent sre)
:该方法用于通知监听器,已经消除一个request对象。
在request对象初始化或者结束前,会自动调用requestInitialized()方法和requestDestroyed()方法,并传入ServletRequestEvent参数,它封装了ServletRequest对象,可以通过ServletRequestEvent的getServletContext()方法取得Servlet上下文对象,通过getServletRequest()方法得到请求对象。
在Web应用程序中,实现ServletRequestListener的方法有两种,形式如下。
- 注入方式监听
@WebListener
public class MyServletRequestListener implements ServletRequestListener{
}
- web.xml配置
<listener>
<listener-class>
com.wujialiang.MyServletRequestListener
</listener-class>
</listener>
ServletRequestAttributeListener
ServletRequestAttributeListener是“Request属性改变监听器”,可以用来监听Request对象加入属性、移除属性或者替换属性时响应的动作事件。
ServletRequestAttributeListener接口的类是javax.servlet…http.ServletRequestAttributeListener,该接口提供3个监听方法。
default void attributeAdded(ServletRequestAttributeEvent srae)
:该方法用于通知监听器,已经在Request中添加一个对象或者变量。default void attributeRemoved(ServletRequestAttributeEvent srae)
:该方法用于通知监听器,已经在Request中移除一个对象或者变量。default void attributeReplaced(ServletRequestAttributeEvent srae)
:该方法用于通知监听器,已经在Request中替换一个对象或者变量。
当对request范围的对象或者变量进行操作时,Web容器会自动调用与实现接口类相对应的方法。ServletRequestAttributeEvent是一个对象,可以利用其getName()方法得到操作对象或者变量的名称,利用get Value()方法得到操作对象或者变量的值。
在Web应用程序中,实现ServletRequestAttributeListener的方法同样有两种,形式如下。- 注入方式监听
@WebListener
public class MyServletRequestAttributeListener implements HttpSessionAttributeListener{
}
- web.xml配置
<listener>
<listener-class>
com.wujialiang.MyServletRequestAttributeListener
</listener-class>
</listener>
maven引入两个包
<dependency>
<groupId>jstl</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>taglibs</groupId>
<artifactId>standard</artifactId>
<version>1.1.2</version>
</dependency>
MyRequestListener .java
package com.wujialiang;
import javax.servlet.ServletRequestAttributeEvent;
import javax.servlet.ServletRequestAttributeListener;
import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.annotation.WebListener;
@WebListener
public class MyRequestListener implements ServletRequestListener, ServletRequestAttributeListener {
public void requestInitialized(ServletRequestEvent sre) {
System.out.println("产生一个新的请求");
}
public void requestDestroyed(ServletRequestEvent sre) {
System.out.println("一个请求消亡了");
}
public void attributeAdded(ServletRequestAttributeEvent srae) {
System.out.println("新增一个request属性,名称为:" + srae.getName() + ",其值为:" + srae.getValue());
}
public void attributeRemoved(ServletRequestAttributeEvent srae) {
System.out.println("移除一个request属性,名称为:" + srae.getName());
}
public void attributeReplaced(ServletRequestAttributeEvent srae) {
System.out.println("修改一个request属性,名称为:" + srae.getName());
System.out.println("修改前的值为:" + srae.getValue());
}
}
index.jsp
<%@ page language="java" import="java.util.*" pageEncoding="utf-8" %>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%
String path = request.getContextPath();
%>
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<p>使用requestlistener监听器</p>
<c:set value="zhangsan" var="username" scope="request"></c:set>
<h1>姓名为:<c:out value="${requestScope.username}"></c:out></h1>
<c:remove var="username" scope="request"></c:remove>
</body>
</html>
过滤器
过滤器概念
何为过滤器?顾名思义,它的作用就是阻挡某些事件的发生。在Wb应用程序中,过滤器是介于Servlet之前,既可以拦截、过滤浏览器的请求,也可以改变对浏览器的响应。它在服务器端与客户端起到了一个中间组件的作用,对二者之间的数据信息进行过滤,其处理过程如下图所示。
由下图可以看出,当客户端浏览器发起一个请求时,服务器端的过滤器将检查请求数据中的内容,它可改变这些内容或者重新设置报头信息,再转发给服务器上被请求的目标资源,处理完毕后再向客户端响应处理结果。
个Wb应用程序,可以有多个过滤器,组成一个过滤器链,如经常使用过滤器完成字符编码的设定和验证用户的合法性。过滤器链中的每个过滤器都各司其职地处理并转发数据。
一般而言,在Wb开发中,经常利用过滤器来实现如下功能:
- 对用户请求进行身份认证。
- 对用户发送的数据进行过滤或者替换。
- 转换图像的数据格式。
- 数据压缩。
- 数据加密。
- XML数据的转换。
- 修改请求数据的字符集。
- 日志记录和审核。
实现与设置过滤器
在Servlet中要实现过滤器,必须实现Filter接口,并用注入的方式或者在web.xml中定义过滤器,让Web容器知道应该加载哪些过滤器。
Filter接口
Filter接口的类是javax.servlet.Filter,该接口有3个方法。
default void init(FilterConfig filterConfig)
:该方法用来初始化过滤器。filterConfig参数是一个FilterConfig对象。利用该对象可以得到过滤器中初始化的配置参数信息。public void doFilter(ServletRequest request,ServletResponse response,FilterChain chain)
:该方法是过滤器中主要实现过滤的方法。当客户端请求目标资源时,Wb应用程序会调用与此目标资源相关的doFilterO方法,在该方法中,实现对请求和响应的数据处理。参数request表示客户端的请求,response表示对应请求的响应,chain是过滤器链对象。在该方法中的特定操作完成后,可调用FilterChain对象的doFilter(request,…response)将请求传给过滤器链中的下一个过滤器,也可以直接返回响应内容,还可以将目标重定向。default void destroy()
:该方法用于释放过滤器中使用的资源。
FilterConfig接口
过滤器中还有FilterConfig接口,该接口用于在过滤器初始化时由Web容器向过滤器传送初始化配置参数,并传入到过滤器对象的initO方法中。FilterConfig接口中有4个方法可以调用。
public String getFilterName()
:用于得到过滤器的名字。public String getlnitParameter(String name)
:得到过滤器中初始化的参数值。public Enumeration<String>getInitParameterNames
(:得到过滤器配置中的所有初始化参数名字的枚举类型。public ServletContext getServletContext()
:得到Servlet上下文文件对象。
设置过滤器
若想实现过滤器,有两种方法:注入或者在web.xml中配置,其形式如下。
- 注入方式
@WebFilter(
description ="demo",
filterName "myfilter",
servletNames ="*.do",
urlPatterns ={"/*"}
initParams ={
@WebInitParam(name ="param",value ="paramvalue")
},
dispatcherTypes={DispatcherType.REQUEST}
- web.xml方式
<filter>
<description>demo</description>
<!--过滤器名称-->
<filter-name>myfilter</filter-name>
<!--过滤器类-->
<filter-class>com.eshore.MyFilter</filter-class>
<!--过滤器初始化参数-->
<init-param>
<param-name>param</param-name>
<param-value>paramvalue</param-value>
</init-param>
</filter>
<!--过滤器映射配置-->
<filter-mapping>
<filter-name>myfilter</filter-name>
<servlet-name>*.do</servlet-name>
<url-pattern>/*</url-pattern>
<dispatcher>REQUEST</dispatcher>
</filter-mapping>
上述的两个配置过程是等价的,在Servlet3.0及以后的版本中可以同时兼容,在Servlet2.0中只能在web.xml中配置。
@WebFilter的主要属性如下表所示。
属性名 | 描述 |
---|---|
value | 该属性与urlPatterns属性等价,但两个属性不能同时使用 |
urlPatterns | 指定Filter的URL匹配模式,等价于<url-pattern>标签 |
filterName | 指定Filter的name属性,等价于<filter-name>标签 |
servletNames | 指定Filter的Servlet过滤对象,等价于<servlet-name>标签。当与urlPatterns同时存在时,则Web容器先比对urlPatterns中的URL,再比对servletNames中的配置 |
dispatcherTypes | 指定Filter的过滤时间,等价于<dispatcher>标签,其值有FORWARD、NCLUDE、REQUEST、ERROR、ASYNC等,默认值是REQUEST |
asyncSupported | 声明Servlet是否支持异步操作模式,等价于<async-supported>标签,该属性在Servlet3.0及以后的版本中才有 |
initParams | 设置过滤器的初始参数 |
请求封装器
请求封装器是指利用HttpServletRequestWrapper类将请求中的内容进行统一修改,例如修改请求字符编码、替换字符、权限验证等。
新建EncodingFilter.java
package com.wujialiang;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.annotation.WebInitParam;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebFilter(
description = "字符串编码过滤器",
filterName = "encodingFilter",
urlPatterns = {"/*"},
initParams = {
@WebInitParam(name = "ENCODING", value = "utf-8")
}
)
public class EncodingFilter implements Filter {
private String encodingName = "";
private String filterName = "";
public void init(FilterConfig filterConfig) throws ServletException {
//通过filterconfig获取初始化的值
encodingName = filterConfig.getInitParameter("ENCODING");
filterName = filterConfig.getFilterName();
if (encodingName == "" || "".equals(encodingName)) {
encodingName = "utf-8";
}
System.out.println("获取编码值");
}
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain filterChain) throws IOException, ServletException {
//转换
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) resp;
//分辨对请求和响应做编码设置
request.setCharacterEncoding(encodingName);
response.setCharacterEncoding(encodingName);
response.setContentType("text/html;charset="+encodingName);
System.out.println("请求被" + filterName + "过滤");
filterChain.doFilter(request, response);
System.out.println("响应被" + filterName + "过滤");
}
public void destroy() {
System.out.println("过滤器销毁");
}
}
上面注解配置相当于下面的xml配置
<filter>
<description>字符串编码过滤器</description>
<filter-name>encodingFilter</filter-name>
<filter-class>com.wujialiang.EncodingFilter</filter-class>
<init-param>
<param-name>ENCODING</param-name>
<param-value>UTF-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-lname>encodingFilter</filter-lname>
<url-pattern>/*</url-pattern>
</filter-mapping>
上述编码过滤器对于post请求是没有问题的,但是对于gt请求获取中文参数时还是会出现乱码问题。这是因为利用post方式请求时,参数是在请求数据包的消息体中;而对于gt请求,参数存放在请求数据包的URI字段中。“request.setCharacterEncoding(encoding);”只对消息体中的数据起作用,对URI字段中的参数不起作用。基于这种情况,可到请求包装器包装请求,将字符编码转换的工作添加到getParameter(方法中,这样就可以对请求的参数进行统一转换。
新建requestEncodingWrapper.java
package com.wujialiang;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.nio.charset.StandardCharsets;
public class requestEncodingWrapper extends HttpServletRequestWrapper {
private String encoding = "";
public requestEncodingWrapper(HttpServletRequest request) {
super(request);
}
public requestEncodingWrapper(HttpServletRequest request, String encoding) {
super(request);
this.encoding = encoding;
}
public String getParameter(String name){
String value = getRequest().getParameter(name);
try{
if(value!=null&&!"".equals(value)){
value = new String(value.trim().getBytes(StandardCharsets.ISO_8859_1),encoding);
}
}catch (Exception e){
e.printStackTrace();
}
return value;
}
}
修改EncodingFilter过滤器
package com.wujialiang;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.annotation.WebInitParam;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebFilter(
description = "字符串编码过滤器",
filterName = "encodingFilter",
urlPatterns = {"/*"},
initParams = {
@WebInitParam(name = "ENCODING", value = "utf-8")
}
)
public class EncodingFilter implements Filter {
private String encodingName = "";
private String filterName = "";
public void init(FilterConfig filterConfig) throws ServletException {
//通过filterconfig获取初始化的值
encodingName = filterConfig.getInitParameter("ENCODING");
filterName = filterConfig.getFilterName();
if (encodingName == "" || "".equals(encodingName)) {
encodingName = "utf-8";
}
System.out.println("获取编码值");
}
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain filterChain) throws IOException, ServletException {
//转换
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) resp;
//分辨对请求和响应做编码设置
if (request.getMethod().equals("GET")) {
request = new requestEncodingWrapper(request, encodingName);
} else {
request.setCharacterEncoding(encodingName);
}
response.setCharacterEncoding(encodingName);
response.setContentType("text/html;charset=" + encodingName);
System.out.println("请求被" + filterName + "过滤");
filterChain.doFilter(request, response);
System.out.println("响应被" + filterName + "过滤");
}
public void destroy() {
System.out.println("过滤器销毁");
}
}
新建Test01Servlet.java
package com.wujialiang;
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.io.PrintWriter;
@WebServlet(name = "test01", urlPatterns = {
"/test01"
})
public class Test01Servlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String name = req.getParameter("name");
System.out.println("name的值为:" + name);
PrintWriter writer = resp.getWriter();
writer.println("你好");
}
}
访问http://localhost:8080/web02/test01?name=%E5%B0%8F%E6%98%8E
加上过滤器之后访问
默认应该是utf-8编码所以不用修改
响应封装器
响应封装器是指利用HttpServletResponse Wrapper类将响应中的内容进行统一修改,例如压缩输出内容、替换输出内容等。有些时候需要对网站的输出内容进行控制,一般有两种方法:一是在保存数据库前对不合法的内容进行替换:二是在输出端进行替换。若是对每一个Servlet都进行输出控制,则任务量将非常大而且烦琐。可利用过滤器对Servlet进行统一处理,但是因为HttpServletResponse不能缓存输出内容,所以需要自定义一个具备缓存功能的response。下面通过两个例子说明响应封装器的实现。
字符替换
修改pom/.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>web02</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<name>web02 Maven Webapp</name>
<!-- FIXME change it to the project's website -->
<url>http://www.example.com</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-servlet-api</artifactId>
<version>9.0.74</version>
</dependency>
</dependencies>
<build>
<finalName>web02</finalName>
<pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
<plugins>
<plugin>
<artifactId>maven-clean-plugin</artifactId>
<version>3.1.0</version>
</plugin>
<!-- see http://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_war_packaging -->
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>3.0.2</version>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
<configuration>
<source>17</source>
<target>17</target>
</configuration>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.1</version>
</plugin>
<plugin>
<artifactId>maven-war-plugin</artifactId>
<version>3.2.2</version>
</plugin>
<plugin>
<artifactId>maven-install-plugin</artifactId>
<version>2.5.2</version>
</plugin>
<plugin>
<artifactId>maven-deploy-plugin</artifactId>
<version>2.8.2</version>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
新建Test02Servlet
package com.wujialiang;
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.io.PrintWriter;
@WebServlet(
name = "test02",
urlPatterns = {
"/test02"
}
)
public class Test02Servlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html;utf-8;");
PrintWriter writer = resp.getWriter();
writer.println("<p>色情</p>");
writer.println("<p>情色</p>");
writer.println("<p>赌博</p>");
writer.flush();
writer.close();
}
}
新增ResponseReplaceWrapper
package com.wujialiang;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import java.io.CharArrayWriter;
import java.io.PrintWriter;
public class ResponseReplaceWrapper extends HttpServletResponseWrapper {
private CharArrayWriter charArrayWriter = new CharArrayWriter();
public ResponseReplaceWrapper(HttpServletResponse response) {
super(response);
}
public PrintWriter getWriter(){
return new PrintWriter(charArrayWriter);
}
public CharArrayWriter getCharArrayWriter(){
return charArrayWriter;
}
}
resources下新建replace.properties
\u8272\u60c5=****
\u60c5\u8272=****
\u8d4c\u535a=****
新建ReplaceFilter.java
package com.wujialiang;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.annotation.WebInitParam;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.util.Properties;
@WebFilter(
description = "内容替换过滤器",
filterName = "replaceFilter",
urlPatterns = {
"/*"
},
initParams = {
@WebInitParam(name = "filePath", value = "replace.properties")
}
)
public class ReplaceFilter implements Filter {
private Properties propert = new Properties();
public void init(FilterConfig filterConfig) throws ServletException {
String filePath = filterConfig.getInitParameter("filePath");
try{
InputStream resourceAsStream = ReplaceFilter.class.getClassLoader().getResourceAsStream(filePath);
propert.load(resourceAsStream);
}catch(Exception e){
e.printStackTrace();
}
}
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletResponse res = (HttpServletResponse)servletResponse;
ResponseReplaceWrapper responseReplaceWrapper = new ResponseReplaceWrapper(res);
filterChain.doFilter(servletRequest,responseReplaceWrapper);
String outString =responseReplaceWrapper.getCharArrayWriter().toString();
for (Object o :propert.keySet()){
String key =(String)o;
outString = outString.replace(key,propert.getProperty(key));
}
PrintWriter writer = res.getWriter();
writer.write(outString);
}
public void destroy() {
}
}
开启过滤器之后
gzip压缩
首先,编写一个自定义的ServletOutputStream类使它具有压缩功能,这里的压缩功能采用GZIP算法实现,这是现在主流浏览器都可以接受的压缩格式,应用JDK自带的GZIPOutputStream类来完成,其源代码如下:
package com.wujialiang;
import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.zip.GZIPOutputStream;
public class GZIPResponseStream extends ServletOutputStream {
//将压缩后的数据存放到ByteArratOutputStream
protected ByteArrayOutputStream byteArrayOutputStream = null;
//JDK自带的GZIP压缩类
protected GZIPOutputStream gzipOutputStream = null;
protected boolean closed = false;
protected HttpServletResponse response = null;
protected ServletOutputStream outputStream = null;
public GZIPResponseStream(HttpServletResponse response) throws IOException {
super();
this.response = response;
this.closed = false;
this.outputStream = response.getOutputStream();
byteArrayOutputStream = new ByteArrayOutputStream();
gzipOutputStream = new GZIPOutputStream(byteArrayOutputStream);
}
@Override
public void close() throws IOException {
if (this.closed) {
throw new IOException("This output stream has already been closed");
}
//执行压缩必须调用这个方法
gzipOutputStream.finish();
//将压缩后的数据输出到浏览器中
byte[] bytes = byteArrayOutputStream.toByteArray();
//设置压缩算法为gzip,浏览器会自动解压数据
response.addHeader("Content-Length", Integer.toString(bytes.length));
response.addHeader("Content-Encoding", "gzip");
//输出到浏览器
outputStream.write(bytes);
outputStream.flush();
closed = true;
}
@Override
public void flush() throws IOException {
if (closed) {
throw new IOException("This output stream has already been closed");
}
gzipOutputStream.flush();
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setWriteListener(WriteListener writeListener) {
}
@Override
public void write(int b) throws IOException {
if (closed) {
throw new IOException("This output stream has already been closed");
}
gzipOutputStream.write((byte) b);
}
@Override
public void write(byte[] b) throws IOException {
write(b, 0, b.length);
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
if (closed) {
throw new IOException("This output stream has already been closed");
}
gzipOutputStream.write(b, off, len);
}
public boolean closed() {
return this.closed;
}
}
然后,自定义response包装类GZIPResponse Wrapper,它只对输出的内容进行压缩,不进行将内容输出到客户端的操作。因为response要处理的不单单是字符内容,还有压缩的内容,即二进制内容,所以它需要重写getOutputStream(和getWriter()方法,其源代码如下:
package com.wujialiang;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
public class GZIPResponseWrapper extends HttpServletResponseWrapper {
//原始response
private HttpServletResponse response = null;
//自定义outputstream对数据进行压缩并且输出
private ServletOutputStream outputStream = null;
//自定义printwriter,将内容输出到servletouputstream
private PrintWriter printWriter = null;
public GZIPResponseWrapper(HttpServletResponse response) {
super(response);
this.response = response;
}
/**
* 利用gzipresponsestream创建输出流
*
* @return
* @throws IOException
*/
public ServletOutputStream createOutputStream() throws IOException {
GZIPResponseStream gzipResponseStream = new GZIPResponseStream(response);
return gzipResponseStream;
}
/**
* 利用这个方法对数据进行gzip压缩,并输出到浏览器中
*/
public void finishResponse() {
try {
if (printWriter != null) {
printWriter.close();
} else {
if (outputStream != null) {
outputStream.close();
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
public void flushBuffer() throws IOException {
outputStream.flush();
}
public ServletOutputStream getOutputStream() throws IOException {
if (printWriter != null) {
throw new IOException("getWriter has been called");
}
if (outputStream == null) {
outputStream = createOutputStream();
}
return outputStream;
}
public PrintWriter getWriter() throws IOException {
if (printWriter != null) {
return printWriter;
}
if (outputStream != null) {
throw new RuntimeException("getOutputStream has already called.");
}
outputStream = createOutputStream();
//通过outputstream获取printWriter方法
printWriter = new PrintWriter(new OutputStreamWriter(outputStream));
return printWriter;
}
/**
* 压缩后数据长度有变化,所以不用重写该方法
* @param length
*/
public void setContentLent(int length){
}
}
接着,编写压缩过滤器类GZIPFilter,过滤器类中通过检查Accept–Encoding标头是否包含gzip字符来判断浏览器是否支持GZIP压缩算法,如果支持,则进行GZIP压缩数据,否则直接输出,其源代码如下:
package com.wujialiang;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Locale;
@WebFilter(
description = "内容替换过滤器",
filterName = "gzipFilter",
urlPatterns = {
"/*"
}
)
public class GZIPFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("GZIPFilter初始化");
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
if (servletRequest instanceof HttpServletRequest) {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
//根据浏览器header信息判断支持的编码格式
String ae = request.getHeader("Accept-Encoding");
if(ae!=null&&ae.toLowerCase().indexOf("gzip")!=-1){
GZIPResponseWrapper gzipResponseWrapper = new GZIPResponseWrapper(response);
filterChain.doFilter(request,response);
gzipResponseWrapper.finishResponse();
return;
}
filterChain.doFilter(request,response);
}
}
@Override
public void destroy() {
}
}
最后,编写一个测试的GzipServlet测试压缩结果。其源代码如下:
package com.wujialiang;
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.io.PrintWriter;
import java.net.URL;
import java.net.URLConnection;
@WebServlet(
name = "gizServlet",
loadOnStartup = 0,
urlPatterns = {
"/gzip"
}
)
public class GZIPServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html;charset=utf-8;");
resp.setCharacterEncoding("utf-8");
PrintWriter out = resp.getWriter();
String[] urls = {
"http://localhost:8080/web02/index.jsp",
"http://localhost:8080/web02/index.css",
"http://localhost:8080/web02/aa.png"
};
for (String url : urls) {
out.println("<p>");
out.println("网址:" + url);
//模拟一个浏览器
URLConnection connGZip = new URL(url).openConnection();
//模拟实质浏览器的表头信息支持GZIP压缩格式
connGZip.setRequestProperty("Accept-Encoding", "gzip");
int lengthGZip = connGZip.getContentLength();
//模拟另一个浏览器
URLConnection connCommon = new URL(url).openConnection();
int lengthCommon = connCommon.getContentLength();
//计算压缩比率
double rate = new Double(lengthGZip) / lengthCommon;
out.println("压缩前数据:" + lengthCommon);
out.println("压缩后数据:" + lengthGZip);
out.println("压缩比率:" + rate);
out.println("</p>");
}
out.close();
}
}
异步处理
在Servlet2.0中,一个普通的Servlet工作流程大致是:首先,Servlet接收请求,对数据进行处理:然后,调用业务接口方法,完成业务处理;最后,将结果返回到客户端。在Servlet中最耗时的是第二步的业务处理,因为它会执行一些数据库操作或者其他的跨网络调用等,在处理业务的过程中,该线程占用的资源不会被释放,这有可能造成性能的瓶颈。
异步处理是Servlet3.0以后新增的一个特性,它可以先释放容器被占用的资源,将请求交给另一个异步线程来执行,业务方法执行完成后再生成响应数据。
本节将讲述异步处理接口AsyncContext的使用和一个异步处理应用实例的实现。
AsyncContext简介
在Servlet3.0中,ServletRequest提供了两个方法来启动AsyncContext:
AsyncContext startAsync()
AsyncContext startAsync(ServletRequest servletRequest,ServletResponse servletResponse)
上述两个方法都能得到AsyncContext接口的实现对象。当一个Servlet调用了startAsync()方法之后,该Servlet的响应就会被延迟,并释放容器分配的线程。AsyncContext接口的主要方法如下表所示。
方法 | 描述 |
---|---|
void addListener(AsyncListener listener) | 添加AsyncListener监听器 |
complete() | 响应完成 |
dispatch() | 指定URL进行响应完成 |
getRequest() | 获取Servlet请求对象 |
getResponse() | 获取Servlet响应对象 |
setTimeout(long timeout) | 设置超时时间 |
start(java.lang.Runnable run) | 异步启动线程 |
在Servlet3.0中,有两种方式支持异步处理:注入声明和web.xml。其形式分别如下所示
- 注入声明
@WebServlet(
asyncSupported-true,
urlPatterns={"/asyncdemo.do"},
name="myAsyncServlet"
}
public class MyAsyncServlet extends HttpServlet{
}
- web.xml中配置
<servlet>
<servlet-name>myAsyncServlet</servlet-name>
<!--异步Servlet类路径-->
<servlet-class>com.eshore.MyAsyncServlet</servlet-class>
<!--异步支持属性-->
<async-supported>true</async-supported>
</servlet>
如果支持异步处理的Servlet前面有Filter,则Filter也需要支持异步处理。
异步处理的的servlet
package com.wujialiang;
import javax.servlet.AsyncContext;
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.io.PrintWriter;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.Executor;
@WebServlet(
asyncSupported = true,
urlPatterns = {
"/asyncdemo"
},
name = "myasyncservlet"
)
public class MyAsyncServlet extends HttpServlet {
SimpleDateFormat sdf = new SimpleDateFormat();
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html;charset=utf-8");
PrintWriter out = resp.getWriter();
out.println("开始时间" + sdf.format(new Date()));
out.flush();
//在子线程中执行作业调度,并由其负责输出响应,主线程退出
AsyncContext asyncContext = req.startAsync(req, resp);
asyncContext.setTimeout(900000000);
new Thread(new Executor(asyncContext)).start();
out.println("结束时间:" + sdf.format(new Date()));
out.flush();
}
public class Executor implements Runnable {
private AsyncContext ctx = null;
public Executor(AsyncContext ctx) {
this.ctx = ctx;
}
@Override
public void run() {
try {
//等待20秒钟,模拟业务方法执行
Thread.sleep(20*1000);
PrintWriter writer = ctx.getResponse().getWriter();
writer.println("业务处理完毕时间:" + sdf.format(new Date()));
writer.flush();
ctx.complete();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
运行时如果报is not surpported错误,可能是因为没有将所有的过滤器或者经过的Servlet都设置成支持异步处理
模拟服务器推送
模拟服务器推送是指模拟由服务器端向客户端推送消息。在HTTP协议中,服务器是无法直接对客户端传送消息的,必须得有一个请求服务器端才能够响应。可以利用Servlet3.0以后的异步处理技术,主动推送消息到客户端。下面以一个例子说明这种技术的实现过程。
首先,编写一个负责存储消息的队列类ClientService,该类的作用是利用Queue添加异步所有的AsyncContext对象,利用BlockingQueue阻塞队列存储页面请求的消息队列,当Queue队列中有数据时,启动一个线程,将BlockingQueue阻塞的内容输出到页面中。ClientService的源代码如下:
package com.wujialiang;
import javax.servlet.AsyncContext;
import java.io.PrintWriter;
import java.util.Queue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.LinkedBlockingQueue;
public class ClientService {
//异步Servlet上下文队列
private final Queue<AsyncContext> ASYNC_QUEUE = new ConcurrentLinkedDeque<>();
//消息队列
private final BlockingQueue<String> INFO_QUEUE = new LinkedBlockingQueue<>();
private ClientService() {
new ClientThread().start();
}
private static ClientService instance = new ClientService();
public static ClientService getInstance() {
return instance;
}
/**
* 添加异步servlet上下文
*
* @param asyncContext
*/
public void addAsyncContext(final AsyncContext asyncContext) {
ASYNC_QUEUE.add(asyncContext);
}
/**
* 添加异步servlet上下文
*
* @param asyncContext
*/
public void removeAsyncContext(final AsyncContext asyncContext) {
ASYNC_QUEUE.remove(asyncContext);
}
/**
* 发送消息到异步线程,最终输出到httpresponse流
*
* @param str
*/
public void callClient(final String str) {
try {
INFO_QUEUE.put(str);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 将数据发送到response流上
*/
protected class ClientThread extends Thread {
public void run() {
boolean done = false;
while (!done) {
try {
//当消息队列中有数据时,取出数据
final String script = INFO_QUEUE.take();
for (AsyncContext ac : ASYNC_QUEUE) {
try {
PrintWriter writer = ac.getResponse().getWriter();
writer.println(script);
writer.flush();
} catch (Exception e) {
ASYNC_QUEUE.remove(ac);
}
}
} catch (Exception e) {
done = true;
throw new RuntimeException(e);
}
}
}
}
}
其次,编写异步的Servlet类,该类的作用是将客户端注册到发送消息的监听队列中,当产生超时、错误等事件时,将异步上下文对象从队列中移除。同时当访问该Servlet的客户端时,在ASYNC_QUEUE中注册一个AsyncContext对象,这样当服务端需要调用客户端时,就会输出AsyncContext内容到客户端
package com.wujialiang;
import javax.servlet.AsyncContext;
import javax.servlet.AsyncEvent;
import javax.servlet.AsyncListener;
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;
@WebServlet(
name = "asyncontext",
urlPatterns = {
"/asynccontextservlet"
},
asyncSupported = true
)
public class AsyncContextServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setCharacterEncoding("utf-8");
resp.setContentType("text/html;charset=utf-8");
final AsyncContext asyncContext = req.startAsync();
//设置asynccontext对象超时时间
asyncContext.setTimeout(10000000);
asyncContext.addListener(new AsyncListener() {//添加异步监听器
@Override
public void onComplete(AsyncEvent asyncEvent) throws IOException {
ClientService.getInstance().removeAsyncContext(asyncContext);
}
@Override
public void onTimeout(AsyncEvent asyncEvent) throws IOException {
ClientService.getInstance().removeAsyncContext(asyncContext);
}
@Override
public void onError(AsyncEvent asyncEvent) throws IOException {
ClientService.getInstance().removeAsyncContext(asyncContext);
}
@Override
public void onStartAsync(AsyncEvent asyncEvent) throws IOException {
}
});
//添加异步asynccontext对象
ClientService.getInstance().addAsyncContext(asyncContext);
}
}
为了显示出来,通过一个隐藏的frame去读取这个异步Servlet发出的信息,其源代码如下:
index.jsp
<%@page language="java" import="java.util.*" pageEncoding="UTF-8" %>
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.4/jquery.js"></script>
</head>
<body>
<p>内容展示如下:</p>
<textarea name="result" id="result" readonly wrap="off">
</textarea>
<iframe id="autoFrame" style="display: none;" src="<%=request.getContextPath()%>/asynccontextservlet"></iframe>
</body>
<script type="text/javascript">
function update(data) {
var result = $("#result")[0];
result.value = result.value + data + "\n";
}
</script>
</html>
新建Test01Servlet触发消息
package com.wujialiang;
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;
@WebServlet(
name = "test01",
urlPatterns = {
"/test01"
}
)
public class Test01Servlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
for (int i = 0; i < 20; i++) {
final String str = "<script>window.parent.update(\"" + String.valueOf(i) + "\");</script>";
ClientService.getInstance().callClient(str);
try {
Thread.sleep(2 * 1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if (i == 10) {
break;
}
}
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req, resp);
}
}
先访问index.jsp,然后访问test01接口
Registration动态注入基础
前面的章节中介绍了Servlet的两种配置方式,一种是通过注解进行注入,一种是通过web.xml进行配置。其中,注入方式是Servlet3.0之后新增的特性。实现动态注入的基础是Registration接口。该接口是Servlet3.0后引入的接口,主要用于向ServletContext中动态注Servlet、.Filter的实例,从而减轻web.xml繁重的配置。
Registration接口的方法
返回值 | 方法 | 说明 |
---|---|---|
java.lang.String | getClassName() | 返回类名 |
java.lang.String | getInitParameter(java.lang.String name) | 根据参数name获取启动时的初始化参数 |
java.util.Map<String> | getlnitParameters() | 获取所有的初始化参数和值,封装到map中 |
java.lang.String | getName() | 返回对应的Servlet或Filter对应的name |
boolean | setInitParameter(java.lang.String name,java.lang.String value) | 设置单个初始化参数 |
java.util.Set<String> | setInitParameters(java.util.Map<java.lang.String,java.lang.String>initParameters) | 批量设置初始化参数 |
ServletRegistration接口和FilterRegistration接口分别继承了Registration接口,并且添加了各自的内容。
ServletRegistration接口中添加了addMapping、getMappings、getRunAsRole方法,在ServletRegistration.Dynamic接口中,添加了setLoadOnStartup、setMultipartConfig等接口,这些信息与之前@WebServlet注解中介绍的属性内容一致。
FilterRegistration接口中添加了addMappingForServletNames、addMappingForUrlPatterns、getServletNameMappings、getUrlPatternMappings方法,这些信息与之前@WebFilter注解中介绍的属性内容一致。