JSP&Servlet学习笔记
Servlet进阶API、过滤器与监听器
Servlet进阶API
- 每个Servlet都必须由web容器读取Servlet设置信息(无论使用标注还是web、xml)初始化等,才可以真正成为一个Servlet。对于每个Servlet的设置信息,web容器会为其生成一个ServletConfig作为代表对象,可以从该对象取得Servlet初始参数,以及代表整个web应用程序的ServletContext对象。
- 本节将以讨论Servlet的生命周期为开始,知道ServletConfig如何设置给Servlet,如何设置为取得Servlet初始参数,以及如何使用ServletContext。
Servlet、ServletConfig月GenericServlet
-
在Servlet接口上,定义了与Servlet生命周期及请求服务相关的init()、service()与destroy()三个方法。前面介绍过,每次请求来到容器时,会产生HttpServletRequest与HttpServletResponse对象,并在调用service()方法时当作参数传入。
-
在Web容器启动后,会读取Servlet设置信息,将Servlet类加载并实例化,并为每个Servlet设置信息产生一个ServletConfig对象,而后调用了Servlet接口的init()方法,并将产生的ServletConfig对象当做参数传入。
-
这个过程只会在创建Servlet实例后发生一次,之后每次请求到来,就调用Servlet实例的service()方法进行服务。
-
ServletConfig即每个Servlet设置的代表对象,容器会为每个Servlet设置信息产生一个Servlet及ServletConfig实例,GenericServlet同时实现了Servlet及ServletConfig。
- GenericServlet主要的目的,就是将初始Servlet调用init()方法传入的ServletConfig封装起来:
private transient ServletConfig config;
public void init(ServletConfig config) throws ServletException{
this.config=config;
this.init();
}
public void init() throws ServletException{
}
- GenericServlet在实现Servlet的init()方法中,也调用了另一个无参数的init()方法,在编写Servlet时,如果有一些初始时所要运行的动作,可以重新定义这个无参数的init()方法,而不是直接重新定义有ServletConfig参数的init()方法。
当有一些对象实例化之后所要进行的操作,必须定义构造器。在编写Servlet时,若想要运行与web应用程序资源相关的初始化动作,则要重新定义init()方法。举例来说,若想要使用ServletConfig来做一些事情,则不能在构造器中定义,因为实例化Servlet时,容器还没有调用init()方法传入ServletConfig,所以不会有ServletConfig实例。
- GenericServlet也包括了Servlet与ServletConfig所定义方法的简单实现,实现内容主要是通过ServletConfig来取得一些相关信息。
public ServletConfig getServletConfig()
{
return config;
}
public String getInitParameter(String name)
{
return getServletConfig().getInitParameter(name);
}
public Enumeration getInitParameterNames()
{
return getServletConfig().getInitParameterNames();
}
public ServletContext getServletContext()
{
return getServletConfig().getServletContext();
}
- 因此在继承HttpServlet实现Servlet时,就可以通过这些方法来取得所要的相关信息,而不用直接意识到ServletConfig的存在。
使用ServletConfig
- ServletConfig相当于个别Servlet的设置信息代表对象,这意味着可以从ServletConfig中取得Servlet设置信息。ServletConfig定义了getInitParameter()、getInitParameterNames()方法,可以取得设置Servlet时的初始参数。
- 若要使用标注设置个别Servlet的初始参数,可以在@WebServlet中使用@WebInitParam设置initParams属性。例如:
@WebServlet(name="ServletConfigDemo", urlPatterns={
"/conf"},
initParams={
@WebInitParam(name="PARAM1",value="VALUE1"),
@WebInitParam(name="PARAM2",value="VALUE2")
})
public class ServletConfigDemo extends HttpServlet{
private String PARAM1;
private String PARAM2;
public void init() throws ServletException{
PARAM1 = getServletConfig().getInitParameter("PARAM1");
PARAM2 = getServletConfig().getInitParameter("PARAM2");
}
}
- 若要在web.xml中设置个别Servlet初始参数,可以在<servlet>标签中使用<init-param>等标签进行设置,web.xml中的设置会覆盖标注的设置。
<servlet>
<servlet-name>ServletConfigDemo</servlet-name>
<servlet-class>chapter5.ServletConfigDemo</servlet-class>
<init-param>
<param-name>PARAM1</param-name>
<param-value>VALUE1</param-value>
</init-param>
<init-param>
<param-name>PARAM2</param-name>
<param-value>VALUE2</param-value>
</init-param>
</servlet>
- 由于ServletConfig必须在Web容器将Servlet实例化之后,调用有参数的init()方法再将之传入,是与Web应用程序资源相关的对象,所以在继承HttpServlet之后,通常会重新定义无参数的init()方法以进行Servlet初始参数的取得。GenericServlet定义了一些方法,将ServletConfig封装起来,便于取得设置信息,所以取得Servlet初始参数的代码也可以写为:
@WebServlet(name="ServletConfigDemo", urlPatterns={
"/conf"},
initParams={
@WebInitParam(name="PARAM1",value="VALUE1"),
@WebInitParam(name="PARAM2",value="VALUE2")
})
public class AddMessage extends HttpServlet{
private String PARAM1;
private String PARAM2;
public void init() throws ServletException{
PARAM1=getInitParameter("PARAM1");
PARAM2=getInitParameter("PARAM2");
}
}
- 下面这个范例简单示范了如何设置、使用Servlet初始参数,其中登录成功与失败的网页,可以由初始参数设置来确定
package chapter5;
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;
@WebServlet(
name = "Login",
urlPatterns = {
"/login.do"},
initParams = {
@WebInitParam(name = "SUCCESS", value = "success.view"),
@WebInitParam(name = "ERROR", value = "error.view")
}
)
public class Login extends HttpServlet {
private String SUCCESS_VIEW;
private String ERROR_VIEW;
@Override
public void init() throws ServletException {
SUCCESS_VIEW = getInitParameter("SUCCESS");
ERROR_VIEW = getInitParameter("ERROR");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html;charset=UTF-8");
String name = req.getParameter("name");
String passwd = req.getParameter("passwd");
if("lancibe".equals(name) && "123456".equals(passwd))
{
req.getRequestDispatcher(SUCCESS_VIEW).forward(req, resp);
}
else {
req.getRequestDispatcher(ERROR_VIEW).forward(req, resp);
}
}
}
- 注意@WebServlet的name属性设置,如果web.xml文件中的设置要覆盖标注设置,<servlet-name>的设置必须和@WebServlet的name属性相同,如果不设置name属性,默认是类完整名称。程序中使用标注设置默认初始参数,并在init()中读取,成功或失败时所发送的网页URL是由初始参数来决定的。如果想使用web.xml来覆盖这些初始参数设置,则可以如下:
<servlet>
<servlet-name>Login</servlet-name>
<servlet-class>chapter5.Login</servlet-class>
<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>Login</servlet-name>
<url-pattern>/login.do</url-pattern>
</servlet-mapping>
使用ServletContext
- ServletContext接口定义了运行Servlet的应用程序环境的一些行为与观点,可以使用ServletContext实现对象来取得所请求资源的URL、设置与存储属性、应用程序初始参数,甚至动态设置Servlet实例。
- ServletContext本身名称和功能不太对应。因为它以Servlet作为开头,容易被误认为仅是单一Servlet的代表对象。事实上,当整个Web应用程序加载Web容器之后,容器会生成一个ServletContext对象作为整个应用程序的代表,并设置个ServletConfig,只要通过ServletConfig的getServletContext()方法就可以取得ServletContext对象。下面先介绍几个需要注意的方法。
getRequestDispatcher()
- 用来取得RequestDispatcher实例,使用时路径的指定必须以“/”作为开头,这个斜杠代表应用程序环境根目录。取得RequestDispatcher实例之后,就可以进行请求的转发Forward或包含Include。
context.getRequestContext("/page/some.jsp").forward(request, response);
getResourcePaths()
- 如果想要知道Web应用程序的某个目录下有哪些文件,则可以使用getResourcePaths()方法。
for(String avatar : getServletContext().getResourcePaths("/"))
{
...;
}
- 使用时指定路径必须以“/”开头,表示相对于应用程序环境根目录,返回的路径如下所示:
- /welcome.html
- /catalog/
- /catalog/index.html
- /catalog/products.html
- /customer/
- /customer/login.jsp
- /WEB-INF/
- /WEB-INF/web.xml
- /WEN-INF/classes/…
- 可以看到,这个方法会联通WEB-INF的信息都列出来。如果是个目录,则会以“/”做结尾。以下范例使用了getResourcePaths()方法,自动取得avatars目录下的图片路径,并通过<img>标签来显示图片。
package chapter5;
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.io.PrintWriter;
@WebServlet(
urlPatterns = {
"/avatar.view"},
initParams = {
@WebInitParam(name = "AVATAR_DIR", value = "/avatars")
}
)
public class Avatar extends HttpServlet {
private String AVATAR_DIR;
@Override
public void init() throws ServletException {
AVATAR_DIR = getInitParameter("AVATAR_DIR");
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html;charset=UTF-8");
PrintWriter out = resp.getWriter();
out.println("<html>");
out.println("<head>");
out.println("<title>头像显示</title>");
out.println("</head>");
out.println("<body>");
for (String avatar : getServletContext().getResourcePaths(AVATAR_DIR))
{
avatar = avatar.replaceFirst("/", "");
out.println("<img src='" + avatar + "'>");
}
out.println("</body>");
out.println("</html>");
out.close();
}
}
getResourceAsStream()
- 如果想在Web应用程序中读取某个文件的内容,则可以使用getResourceAsStream()方法,使用时指定路径必须以“/”作为开头,表示相对于应用程序环境根目录,或者相对是/WEB-INF/lib中JAR文件里的META-INF/resources的路径,运行结果会返回InputStream实例,接着就可以运用它来读取文件内容。
- 每个Web应用程序都有一个相对应的ServletContext,针对“应用程序”初始化时需用到的一些参数数据,可以在web.xml中设置应用程序初始参数,通常这会结合ServletContextListener来做。
应用程序事件、监听器
- Web容器管理Servlet/JSP相关的对象生命周期,弱队HttpServletRequest对象、HttpSession对象、ServletContext对象在生成、销毁或相关属性设置发生的时机点有兴趣,则可以实现对应的监听器(Listener),做好相关的设置,这样在对应的时机点发生时,Web容器就会调用监听器上相对应的方法,使得我们可以在对应的时机点作出处理。
ServletContext事件、监听器
- 与ServletContext相关的监听器有ServletContextListener与ServletContextAttributeListener。
ServletContextListener
- ServletContextListener是“生命周期监听器,如果想要知道何时Web应用程序已经初始化或者即将结束销毁,可以实现ServletContextListener;
package javax.servlet;
import java.util.EventListener;
public interface ServletContextListener extends EventListener{
public void contextInitialized(ServletContextEvent sce);
public void contextDestroyed(ServletContextEvent sce);
}
- 在Web应用程序初始化或者即将结束销毁前,会调用ServletContextListener实现类相对应的contextInitialized()或contextDestroyed()。可以在contextInitialized()中实现应用程序资源的准备动作,在contextDestroyed()实现释放应用程序资源的动作。
- 例如,可以实现ServletContextListener,在应用程序初始过程中,准备好数据库连线对象、读取应用程序设置等动作,如放置使用头像的目录信息,就不宜将目录名称写死,以免日后目录变动名称或位置时,所有相关的Servlet都需要进行源代码的修改。这时可以这么做:
package chapter5;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
@WebListener // 使用@WebListener标注
public class ContextParameterReader implements ServletContextListener {
public void contextInitialized(ServletContextEvent sce)
{
ServletContext context = sce.getServletContext();
String avatars = context.getInitParameter("AVATAR");
context.setAttribute("avatars", avatars);
}
public void contextDestroyed(ServletContextEvent sce){
}
}
- ServletContextListener可以直接使用@WebListener标注,而且必须实现ServletContextListener接口,这样容器就会在启动时加载并运行对应的方法。当Web容器调用contextInitialized()或contextDestroyed()时,会传入ServletContextEvent,其封装了ServletContext,可以通过ServletContextEvent的getServletContext()方法取得ServletContext,通过ServletContext的getInitParameter()方法来读取初始参数,因此Web应用程序初始参数常被称为ServletContext初始参数。
- 在整个Web应用程序生命周期,Servlet需共享的资料可以设置为ServletContext属性。由于ServletContext在Web应用程序存活期间都会一直存在,所以设置为ServletContext属性的数据,除非主动移除,否则也是一直存活在Web应用程序中.
- 可以通过ServletContext的setAttribute()方法设置对象为ServletContext属性,之后可以通过ServletContext的getAttribute()方法取出该属性。若要移除属性,则通过ServletContext的removeAttribute()方法。
- 因为@WebListener没有设置初始参数的属性,所以仅适用于无需设置初始参数的情况。如果需要设置初始参数,可以在web.xml中设置。
<context-param>
<param-name>AVATAR</param-name>
<param-value>/avatars</param-value>
</context-param>
- 在web.xml中,使用<context-param>标签来定义初始参数。由于先前的ContextParameterReader读取的初始参数已经设置为ServletContext属性,因此先前的头像范例必须作出修改:
package chapter5;
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.io.PrintWriter;
@WebServlet(urlPatterns = "/avatar.view")
public class Avatar extends HttpServlet {
private String AVATAR_DIR;
@Override
public void init() throws ServletException {
AVATAR_DIR = (String)getServletContext().getAttribute("avatars");
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html;charset=UTF-8");
PrintWriter out = resp.getWriter();
out.println("<html>");
out.println("<head>");
out.println("<title>头像显示</title>");
out.println("</head>");
out.println("<body>");
for (String avatar : getServletContext().getResourcePaths(AVATAR_DIR))
{
avatar = avatar.replaceFirst("/", "");
out.println("<img src='" + avatar + "'>");
}
out.println("</body>");
out.println("</html>");
out.close();
}
}
- 主要改变就是不需要设置ServletConfig初始参数,以及从ServletContext中取出先前所设置的属性。
- 在web.xml中,也使用了<listener>与<listener-class>标签来定义实现了Servlet-contextListener接口的类名称。
- 有些应用程序的设置,必须在Web应用程序初始时进行,前面谈过,若要改变HttpSession的一些Cookie设置,可以在web.xml中定义。另一个方式,则是取得ServletContext后,使用getSessionCookieConfig()取得SessionCookieConfig进行设置,不过这个动作必须在应用程序初始时进行。
@WebListener()
pubilc class SomeContextListener implements ServletContextListener{
@Override
public void contextInitialized(ServletContextEvent sce)
{
ServletContext context = sce.getServletContext();
context.getSessionCookieConfig().setName("123");
}
public void contextDestroyed(ServletContextEvent sce){
}
}
ServletContextAttributeListener
- ServletContextAttributeListener是“监听属性改变的监听器”,如果想要对象被设置、移除或者替换ServletContext属性,可以收到通知以进行一些操作,则可以实现ServletContextAttributeListener。
public interface ServletContextAttributeListener extends EventListener{
public void attributeAdded(ServletContextAttributeEvent scab);
public void attributeRemoved(ServletContextAttributeEvent scab);
public void attributeReplaced(ServletContextAttributeEvent scab);
}
- 当在ServletContext中添加属性、移除属性、或替换属性时,相对应的attributeAdded()、attributeRemoved()与attributeReplaced()方法就会被调用。
- 如果希望容器在部署应用程序时,实例化实现ServletContextAttributeListener的类并注册给应用程序,同样是在实现类上标注@WebListener,并实现ServletContextAttributeListener接口:
@WebListener()
public class SomeContextAttrListener implements ServletContextAttributeListener{
...;
}
- 当然也可以在web.xml中设置:
<listener>
<listener-class>chapter5.SomeContextAttrListener</listener-class>
</listener>
HttpSession事件、监听器
- 与HttpSession相关的监听器有四个:HttpSessionListener、HttpSessionAttributeListener、HttpSessionBindingListener和HttpSessionActivationListener。
HttpSessionListener
- HttpSessionListener是“生命周期监听器”
public void sessionCreated(HttpSessionEvent se);
public void sessionDestroyed(HttpSessionEvent se);
- 在HttpSession对象初始化或结束之前,会分别调用sessionCreated()与sessionDestroyed()方法,可以通过传入的HttpSessionEvent,使用getSession()取得HttpSession,以针对回话对象做出相对应的创建或结束处理工作。
- 例如,有些网站为了防止用户重复登录,会在数据库中以某个字段代表用户是否登录,用户登录后,在数据库中设置该字段信息,代表用户已登录,而用户注销后,再重置该字段。如果用户已经登录,在注销前尝试用另一个浏览器进行登录,应用程序会检查数据库中代表登录与否的字段,如果发现已被设置为登录,则拒绝用户重复登录。
- 但是,如果用户在注销前不小心关闭浏览器,没有确实运行注销操作,那么数据库中代表登录与否的字段就不会被重置。为此,可以实现HttpSessionListener,由于HttpSession有存活期限,当容器销毁某个HttpSession时,就会调用sessionDestroyed(),就可以在当中判断要重置哪个用户数据库中代表登录与否的字段。
@WebListener
public class ResetLoginHelper implements HttpSessionListener{
public void sessionCreated(HttpSessionEvent se){
}
public void sessionDestroyed(HttpSessionEvent se){
HttpSession session = se.getSession();
String user = session.getAttribute("login");
}
}
- 如果在实现HttpSessionListener的类上标注@WebListener,则容器会在部署应用程序时,实例化并注册给应用程序,另一种方式就是在web.xml中设置。
<listener>
<listener-class>chapter5.ResetLoginHelper</listener-class>
</listener>
- 下面有一个HttpSessionListener的应用实例。假设有个应用程序在用户登陆后使用HttpSession对象来进行会话管理。如:
package chapter5;
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.HashMap;
import java.util.Map;
@WebServlet("/SessionListenerDemoLogin.do")
public class SessionListenerDemoLogin extends HttpServlet {
private Map<String, String> users;
public SessionListenerDemoLogin(){
users = new HashMap<String, String>();
users.put("lancibe", "123456");
users.put("lancibe22", "123456");
users.put("root", "1234567890");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String name = req.getParameter("name");
String passwd = req.getParameter("passwd");
String page = "form.html";
if(users.containsKey(name) && users.get(name).equals(passwd))
{
req.getSession().setAttribute("user", name);
page = "welcome.view";
}
resp.sendRedirect(page);
}
}
- 这个Servlet在用户验证通过后,会取得HttpSession实例并设置属性。如果想要在应用程序中加上显示目前已登录在线人数的功能,则可以实现HttpSessionListener接口。
package chapter5;
import javax.servlet.annotation.WebListener;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
@WebListener
public class OnlineUserCounter implements HttpSessionListener {
private static int counter;
public static int getCounter() {
return counter;
}
@Override
public void sessionCreated(HttpSessionEvent se) {
OnlineUserCounter.counter++;
}
@Override
public void sessionDestroyed(HttpSessionEvent se) {
OnlineUserCounter.counter--;
}
}
- OnlineUserCounter中有个静态变量,每一次HttpSession创建时会递增,销毁时会递减,也就是通过统计HttpSession的实例来做登录用户的计数功能。
- 只要在想要显示人数的页面,使用OnlineUserCounter.getCounter(),就可以取得目前的在线人数并显示。
package chapter5;
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 javax.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
@WebServlet("/SessionListenerDemoWelcome.view")
public class SessionListenerDemoWelcome extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html;charset=UTF-8");
PrintWriter out = resp.getWriter();
HttpSession session = req.getSession(false);
out.println("<html>");
out.println("<head>");
out.println("<title>欢迎</title>");
out.println("</head>");
out.println("<h1>目前在线人数 " + OnlineUserCounter.getCounter() + " 人</h1>");
if(session != null)
{
String user = (String)session.getAttribute("user");
out.println("<h1>欢迎:"+user+"</h1>");
out.println("<a href='logout.do'>注销</a>");
}
out.println("</body>");
out.println("</html>");
out.close();
}
}
HttpSessionAttributeListener
- HttpSessionAttributeListener是“属性改变监听器”,当在会话对象中加入属性、移除属性或替换属性时,相对应的attributeAdded()、attributeRemoved()与attributeReplaced()方法就会被调用,并分别传入HttpSessionBindingEvent。
public interface HttpSessionAttributeListener extends EventListener{
public void attributeAdded(HttpSessionBindingEvent se);
public void attributeRemoved(HttpSessionBindingEvent se);
public void attributeReplaced(HttpSessionBindingEvent se);
}
- HttpSessionBindingEvent有个getName()方法,可以取得属性设置或移除时指定的名称,而getValue()可以取得属性设置或移除时的对象。
- 如果希望容器在部署应用程序时,实例化实现HttpSessionAttributeListener的类并注册给应用程序,则同样也是在实现类上标注@WebListener
@WebListener()
public class HttpSessionAttributeLlistener implements HttpSessionAtributeListener{
}
- 也可以在web.xml下设置
<listener>
<listener-class>chapter5.HttpSessionAttributeListener</listener-class>
</listener>
HttpSessionBindingListener
- HttpSessionBindingListener是“对象绑定监听器”,如果有个即将加入HttpSession的属性对象,希望在设置给HttpSession成为属性或从HttpSession中移除时,可以收到HttpSession的通知,则可以让该对象实现HttpSessionBindingListener接口。
public interface HttpSessionBindingListener extends EventListener{
public void valueBound(HttpSessionBindingEvent event);
public void valueUnbound(HttpSessionBindingEvent event);
}
- 这个接口即是实现加入HttpSession的属性对象,不需要注释火灾web.xml中设置。当实现此接口的属性对象被加入HttpSession或从中移除时,就会调用对应的valueBound()与valueUnbound()方法,并传入HttpSessionBindingEvent对象,可以通过该对象的getSession()取得HttpSession对象。
- 下面介绍这个接口使用的一个范例。假设修改前一个范例程序的登录Servlet如下:
package chapter5;
import chapter5.User;
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.HashMap;
import java.util.Map;
@WebServlet("/SessionListenerDemo2Login.do")
public class SessionListenerDemo2Login extends HttpServlet {
private Map<String, String> users;
public SessionListenerDemo2Login(){
users = new HashMap<String, String>();
users.put("lancibe", "123456");
users.put("lancibe22", "123456");
users.put("root", "1234567890");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String name = req.getParameter("name");
String passwd = req.getParameter("passwd");
String page = "form.html";
if(users.containsKey(name) && users.get(name).equals(passwd))
{
User user = new User(name);
req.getSession().setAttribute("user", user);
page = "SessionListenerDemoWelcome.view";
}
resp.sendRedirect(page);
}
}
- 当用户输入正确的名称、密码时,首先会以用户名来创建User实例,而后加入HttpSession中作为属性。希望User实例被加入成为HttpSession属性时,可以自动从数据库中加载用户的其他数据,如地址、照片等,或是在日志中记录用户登录信息,可以让User类实现HttpSessionBindingListener接口。
package chapter5;
import javax.servlet.http.HttpSessionBindingEvent;
import javax.servlet.http.HttpSessionBindingListener;
public class User implements HttpSessionBindingListener {
private String name;
private String data;
public User(String name)
{
this.name = name;
}
@Override
public void valueBound(HttpSessionBindingEvent event) {
this.data = name + "来自数据库的数据...";
}
@Override
public void valueUnbound(HttpSessionBindingEvent event) {
}
public String getData() {
return data;
}
public String getName() {
return name;
}
}
- 在valueBound()中,可以实现查询数据库的功能(也许是委托给一个负责查询数据库的服务对象),并补齐User对象中的相关数据。当HttpSession失效前会先移除属性,或者主动移除属性时,则valueUnbound()方法会被调用。
HttpSessionActivationListener
- HttpSessionActivationListener是“对象迁移监听器”,定义了两个方法sessionWillPassivate()与sessionDidActivate()。很多情况下,几乎用不到HttpSessionActivationListener。在使用分布式环境时,应用程序的对象可能分散在多个JVM中。当HttpSession要从一个JVM迁移至另一个JVM时,必须先在原本的JVM上序列化(Serialize)所有的属性对象,在这之前若属性对象有实现HttpSessionActivationListener,就会调用sessionWillPassivate()方法,而HttpSession迁移至另一个JVM后,就会对所有属性对象作反序列化,此时会调用sessionDidActivate()方法。
HttpServletRequest事件、监听器
- 与请求相关的监听器有三个:ServletRequestListener、ServletRequestAttributeListener与AsyncListener。
ServletRequestListener
- ServletRequestListener是“生命周期监听器”,如果想要在HttpServletRequest对象生成或结束时做出相对应的操作,则可以实现ServletRequestListener。
public interface ServletRequestListener extends eventListener{
public void requestDestroyed(ServletRequestEvent sre);
public void requestInitialized(ServletRequestEvent sre);
}
- 在ServletRequest对象初始化或结束前,会调用requestInitialized()与requestDestroyed()方法,可以通过传入的ServletRequestEvent来取得ServletRequest,以针对请求对象做出相对应的初始化或结束处理动作。
@WebListener()
public class SomeRequestListener implements ServletRequestListener{
}
- 如果在实现ServletRequestListener的类上标注@WebListener,则容器在部署应用程序时,会实例化类并注册给应用程序。另一个方式是在web.xml中设置:
<listener>
<listener-class>chapter5.SomeRequestListener</listener-class>
</listener>
ServletRequestAttributeListener
- 属性改变监听器,当在会话对象中加入属性、移除属性或替换属性时,相对应的attributeAdded()、attributeRemoved()与attributeReplaced()方法就会被调用,并分别传入HttpSessionBindingEvent。
public interface HttpSessionAttributeListener extends EventListener{
public void attributeAdded(HttpSessionBindingEvent se);
public void attributeRemoved(HttpSessionBindingEvent se);
public void attributeReplaced(HttpSessionBindingEvent se);
}
- HttpSessionBindingEvent有个getName()方法,可以取得属性设置或移除时指定的名称,而getValue()可以取得属性设置或移除时的对象。
- 如果希望容器在部署应用程序时,实例化实现HttpSessionAttributeListener的类并注册给应用程序,则同样也是在实现类上标注@WebListener
@WebListener()
public class HttpSessionAttrListener implements HttpSessionAttributeListener{
}
- 也可以在web.xml下设置
<listener>
<listener-class>chapter5.HttpSessionAttrListener</listener-class>
</listener>
过滤器
- 在容器调用Servlet的service()方法前,Servlet并不会知道有请求到来,而在service()方法运行之后,容器真正对浏览器进行HTTP响应之前,浏览器也不会知道Servlet真正的响应是什么。过滤器(Filter)正如其名称所示,是介于Servlet之前,可阻拦过滤浏览器对Servlet的请求,也可以改变Servlet对浏览器的响应。
- 下面将介绍过滤器的运用概念,了解如何实现Filter接口来编写过滤器,如何在web.xml中设置过滤器、改变过滤器的顺序等,以及如何使用请求封装器(Wrapper)及响应封装器,将容器产生的请求与响应对象加以包装,针对某些请求信息或响应进行加工处理。
过滤器的概念
- 想想已经开发好应用程序的主要功能了,但现在有几个新的需求:
- 针对所有Servlet,产品经理想要了解从请求到响应之间的时间差。
- 针对某些特定的页面,客户希望只有特定几个用户才可以浏览。
- 基于安全方面的考虑,用户输入的特定字符必须过滤并替换为无害的字符。
- 请求与响应的编码从Big5改用UTF-8。
- 以第一个需求而言,也许我们会想到,在doXXX()开头与结尾取得系统时间,来计算时间差,但如果页面有上百个、上千个,怎么完成这些需求呢。如果产品经理在完成需求之后,有要求取消计算时间差的功能,该怎么办?
- 可以先分析一下这些需求:
- 运行Servlet的service()方法“前”,记录起始时间,Servlet的service()方法,运行“后”,记录结束时间,并计算时间差。
- 运行Servlet的service()方法“前”,验证是否为允许的用户。
- 运行Servlet的service()方法“前”,对请求参数进行字符过滤和替换。
- 运行Servlet的service()方法“前”,对请求与响应对象设置编码。
- 经过分析,可以发现这些需求可以在运行Servlet的service()方法“前”与Servlet的service()方法“后”中间进行实现。
- 性能评测、用户验证、字符替换、编码设置等需求,基本上与应用程序的业务需求没有直接的关系,只是应用程序额外的元件服务之一。可能只是短暂需要他,或者需要整个系统应用相同设置,不应该为了一时之需而修改代码加入到原有业务流程之中。
- 因此,这类需求应该设计为独立的元件,随时可以加入应用程序中,也随时可以移除,或随时可以修改设置而不用修改原有的程序。这类元件就像是一个过滤器,安插在浏览器与Servlet中间,可以过滤请求与响应而作进一步的处理。
- Servlet/JSP提供了过滤器机制使得可以完成这些元件服务,可以视需求抽换过滤器或调整过滤器的顺序,也可以针对不同的URL应用不同的过滤器。甚至在不同的Servlet间请求转发或包含时应用过滤器。
实现与设置过滤器
- 在Servlet/JSP中要实现过滤器,必须实现Filter接口,并使用@WebFilter标注或在web.xml中定义过滤器,让容器知道该加载哪些过滤器类。Filter接口有三个要实现的方法:init()、doFilter()与destroy()。
public interface Filter{
public void init(FilterConfig filterConfig) throws ServletException;
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException;
public void destroy();
}
- FilterConfig类似于Servlet接口init()方法参数上的ServletConfig,FilterConfig是实现Filter接口的类上使用标注或web.xml中过滤器设置信息的代表对象。如果在定义过滤器时设置了初始参数,则可以通过FilterConfig的getInitParameter()方法来取得初始参数。
- Filter接口的doFilter()方法类似于Servlet接口的service()方法。当请求来到容器,而容器发现调用Servlet的service()方法前,可以应用某过滤器时,就会调用该过滤器的doFilter()方法。可以在doFilter()方法中进行service()方法的前置处理,而后决定是否调用FilterChain的doFilter()方法
- 如果调用了FilterChain的doFilter()方法,就会运行下一个过滤器,如果没有下一个过滤器了,就会调用请求目标Servlet的service()方法。如果因为某个情况(如用户没有通过验证)而没有调用FilterChain的doFilter(),则请求就不会继续交给接下来的过滤器或目标Servlet,这时就是所谓的拦截请求(从Servlet的观点来看,根本不知道浏览器有发出请求)。FilterChain的doFilter()实现,概念是类似以下:
Filter filter = filterIterator.next();
if(filter != null)
{
filter.doFilter(req, resp, this);
}
else
{
targetServlet.service(req, resp);
}
- 在陆续调用玩Filter实例的doFilter()仍至Servlet的service()之后,流程会以堆栈顺序返回,所以在FilterChain的doFilter()运行完毕后,就可以针对service()方法做后续处理。
- 只需要知道FilterChain运行后会以堆栈顺序返回即可。在实现Filter接口时,不用理会这个Filter前后是否有其他Filter,应该将之作为一个独立的元件设计。
- 如果在调用Filter的doFilter()期间,因故抛出UnavailableException,此时不会继续下一个Filter,容器可以检测异常的isPermanent(),如果不是true,则可以稍后重试Filter。
- 以下实现一个简单的性能评测过滤器,可以用来记录请求与响应之间的时间差,了解Servlet处理请求到响应所需花费的时间。
package chapter5;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;
@WebFilter(filterName = "performance", urlPatterns = {
"/*"})
public class PerformanceFilter implements Filter {
private FilterConfig config;
@Override
public void init(FilterConfig config) throws ServletException{
this.config = config;
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
long begin = System.currentTimeMillis();
filterChain.doFilter(servletRequest, servletResponse);
config.getServletContext().log("Request process in " + (System.currentTimeMillis() - begin)+" milliseconds");
}
@Override
public void destroy() {
}
}
- 当过滤器类被载入容器时并实例化之后,容器会运行其init()方法并传入FilterConfig对象作为参数。在doFilter()的实现中,先记录目前的系统时间,接着调用FilterChain的doFilter()继续接下来的过滤器或Servlet,当FilterChain的doFilter()返回时,取得系统时间并减去先前记录的时间,就是请求与响应间的时间差。
- 过滤器的设置与Servlet的设置很类似。@WebFilter中的filterName设置过滤器名称,urlPatterns设置哪些URL请求必须应用哪个过滤器,可应用的URL模式与Servlet基本相同,而"/*"表示应用在所有的URL请求,过滤器还必须实现Filter接口。
- 如果要在web.xml中设置,则可以如下所示,标注的设置会被web.xml中的设置覆盖。
<filter>
<filter-name>performance</filter-name>
<filter-class>chapter5.PerformanceFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>performance</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
- 除了指定URL模式之外,还可以指定Servlet名称,这可以通过@WebServlet的servletNames来设置:
@WebFilter(filterName="performance", servletNames={
"SomeServlet"})
- 或在web.xml中的<filter-mapping>中使用<servlet-name>来设置
...
<servlet-name>SomeServlet</servlet-name>
...
- 如果想要一次符合所有的Servlet名称,则可以使用星号*,如果在过滤器初始化时,想要读取一些参数,可以在@WebFilter中使用@WebInitParam设置initParams。
@WebFilter(
filterName="performance",
urlPatterns={
"/*"},servletNames={
""},
initParams={
@WebInitParam(name="PARAM1", value="VALUE1"),
@WebInitParam(name="PARAM1", value="VALUE2")
})
- 触发过滤器的时机,默认是浏览器直接发出请求。如果是那些通过RequestDispatcher的forward()或include()的请求,设置@WebFilter的dispatcherTypes。
@WebFilter(
filterName="some",
urlPatterns={
"/some"},
dispatcherTypes={
DispatcherType.FORWARD,
DispatcherType.INCLUDE,
DispatcherType.REQUEST,
DispatcherType.ERROR,
DispatcherType.ASYNC
}
)
- 如果不设置任何的dispatcherTypes,则默认是REQUEST。
- 若要在web.xml中设置,则可以使用<dispatcher>标签。
- 可以通过<url-pattern>或<servlet-name>来指定,哪些URL请求或哪些Servlet可以应用过滤器。如果同时具备<url-pattern>与<servlet-name>,则先比对前者,再比对后者。如果有某个URL或Servlet会应用多个过滤器,则根据<filter-mappting>在web.xml中出现的先后顺序,来决定过滤器的运行顺序。
请求封装器
- 以下举两个实际例子,来说明请求封装器的实现与应用,分别是字符替换过滤与编码设置过滤器。
实现字符替换过滤器
- 假设微博程序已经上线,但是发现,有些用户在留言中输入HTML标签,但是我们并不希望用户在留言中通过输入
<a href="http://www.baidu.com">123</a>
类似这样的信息。 - 希望将一些HTML过滤掉,如将<、>角括号置换为HTML实体字符
<
与>
,如果不想直接修改留言程序,则可以使用过滤器的方式,将用户请求参数中的角括号字符进行替换。但问题在于,虽然可以使用HttpServletRequest的getParameter()取得请求参数值,但是没有一个像setParameter()的方法,可以将处理过后的请求参数重新设置给HttpServletRequest。 - 对于容器产生的HttpServletRequest对象,无法直接修改某些信息,如请求参数值就是一个例子。
- 好在有个HttpServletRequestWrapper实现了HttpServletRequest接口,只要继承HttpServletRequestWrapper类,并编写想要重新定义的方法即可(有点像AWT编程中使用事件适配器和lambda表达式创建事件监听器)。相对应于ServletRequest接口,也有个ServletRequestWrapper类可以使用。
- 以下的范例通过继承HttpServletRequestWrapper实现了一个请求封装器,可以将请求参数中的HTML符号替换为HTML实体字符。
package chapter5;
import org.apache.commons.lang3.StringEscapeUtils;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
public class EscapeWrapper extends HttpServletRequestWrapper {
public EscapeWrapper(HttpServletRequest req)
{
super(req);
}
@Override
public String getParameter(String name) {
String value = getRequest().getParameter(name);
return StringEscapeUtils.escapeHtml4(value);
}
}
- EscapeWrapper类继承了HttpServletRequestWrapper,并定义了一个接受HttpServletRequest的构造器,真正的HttpServletRequest将通过此构造器传入,必须使用super()调用HttpServletRequestWrapper接受HttpServletRequest的构造器,之后如果要取得被封装的HttpServletRequest,则可以调用getRequest()方法。
package chapter5;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
@WebFilter("/*")
public class EscapeFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest requestWrapper = new EscapeWrapper((HttpServletRequest) servletRequest);
filterChain.doFilter(requestWrapper, servletResponse);
}
@Override
public void destroy() {
}
}
- 在Filter的doFilter()中,创建EscapeWrapper实例,并将原请求对象传入构造器进行封装。然后将EscapeWrapper实例穿日FilterChain的doFilter()中作为请求对象。之后的Filter或Servlet实例不需要也不会知道请求对象已经被封装,在必须取得请求参数时,同样调用getParameter()即可。
- 当将这个过滤器挂上去之后,如果有用户试图输入HTML标签,由于角括号都被替换为实体字符,所以出现的留言将会变成文本信息。
实现编码设置过滤器
- 在先前的范例中,如果要设置请求字符编码,都是在个别的Servlet中处理。可以在过滤器中进行字符编码设置,如果日后要改变编码,就不用每个Servlet逐一修改设置。
- 在前面介绍字符编码设置的时候介绍过,HttpServletRequest的setCharacterEncoding()方法是针对请求Body内容,对于GET请求,必须取得请求参数的字节阵列后,重新指定编码建构字符串。这个需求与上一个范例类似,可搭配请求封装器来实现。
package chapter5;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.UnsupportedEncodingException;
public class EncodingWrapper extends HttpServletRequestWrapper {
private String ENCODING;
public EncodingWrapper(HttpServletRequest request, String ENCODING)
{
super(request);
this.ENCODING = ENCODING;
}
@Override
public String getParameter(String name) {
String value = getRequest().getParameter(name);
if(value!= null)
{
try {
byte[] b = value.getBytes("ISO-8859-1");
value = new String(b, ENCODING);
}
catch (UnsupportedEncodingException e)
{
throw new RuntimeException(e);
}
}
return value;
}
}
- 该类的实现与上一个范例类似,其继承了HttpServletRequestWrapper,并定义了一个接受HttpServletRequest的构造器,真正的HttpServletRequest将通过此构造器传入,必须使用super()调用HttpServletRequestWrapper接受HttpServletRequest的构造器,之后如果要取得被封装的HttpServletRequest,则可以调用getRequest()方法。
- 之后若有Servlet要取得请求参数值,都会调用getParameter(),所以这里重新定义了getParameter()方法。在此方法中,将真正从封装的HttpServletRequest对象上取得的请求参数值进行编码替换的动作。
- 编码过滤器的实现如下所示:
package chapter5;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.annotation.WebInitParam;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
@WebFilter(
urlPatterns = {
"/*"},
initParams = {
@WebInitParam(name = "ENCODING", value = "UTF-8")
}
)
public class EncodingFilter implements Filter {
private String ENCODING;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
ENCODING = filterConfig.getInitParameter("ENCODING");
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest)servletRequest;
if("GET".equals(req.getMethod()))
{
req = new EncodingWrapper(req, ENCODING);
}
else
{
req.setCharacterEncoding(ENCODING);
}
filterChain.doFilter(req, servletResponse);
}
@Override
public void destroy() {
}
}
- 请求参数的编码设置是通过过滤器初始参数来设置的,并在过滤器初始化方法init()中读取,过滤器仅在GET请求以创建EncodingWrapper实例,其他方法则通过HttpServletRequest的setCharacterEncoding()设置编码,最后都调用FilterChain的doFilter()方法传入EncodingWrapper实例或原请求对象。
响应封装器
- 在Servlet中,是通过HttpServletResponse对象来对浏览器进行响应的。如果想要对响应内容进行压缩处理,就要想办法让HttpServletResponse对象具有压缩处理的功能。先前介绍过请求封装器的实现,而在响应封装器的部分,可以继承HttpServletResponseWrapper类来对HttpServletResponse对象进行封装。
- 若要对浏览器进行输出响应,必须通过getWriter()取得PrintWriter,或通过getOutputStream()取得ServletOutputStream。所以针对压缩输出的需求,主要就是继承HttpServletResponseWrapper之后,通过重新定义这两个方法来达成。
- 在这里压缩功能将采用GZIP格式,这是浏览器可以接受的压缩格式,可以使用GZIPOurputStream类来实现。由于getWriter()的PrintWriter在创建时,也是必须使用到ServletOutputStream,所以在这里先扩展ServletOutputStream类,使其具有压缩的功能。
package chapter5;
import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import java.io.IOException;
import java.util.zip.GZIPOutputStream;
public class GZipServletOutputStream extends ServletOutputStream {
private GZIPOutputStream gzipOutputStream;
public GZipServletOutputStream(ServletOutputStream servletOutputStream)throws IOException
{
this.gzipOutputStream = new GZIPOutputStream(servletOutputStream);
}
public void write(int b)throws IOException
{
gzipOutputStream.write(b);
}
public GZIPOutputStream getGzipOutputStream() {
return gzipOutputStream;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setWriteListener(WriteListener writeListener) {
}
}
- GzipServletOutputStream继承ServletOutputStream类,使用时必须传入ServletOutputStream类,由GZIPOut普通Stream来增加压缩输出串流的功能。
- 在HttpServletResponse对象传入Servlet的service()方法前,必须封装它,使得调用getOutputStream()时,可以取得这里所实现的GZipServletOutputStream对象,而调用getWriter()时,也可以利用GZipServletOutputStream对象来构造PrintWriter对象。
package chapter5;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.util.zip.GZIPOutputStream;
public class CompressionWrapper extends HttpServletResponseWrapper {
private GZipServletOutputStream gZipServletOutputStream;
private PrintWriter printWriter;
public CompressionWrapper(HttpServletResponse resp)
{
super(resp);
}
@Override
public ServletOutputStream getOutputStream() throws IOException {
if(printWriter != null)
{
throw new IllegalStateException();
}
if(gZipServletOutputStream == null)
{
gZipServletOutputStream = new GZipServletOutputStream(getResponse().getOutputStream());
}
return gZipServletOutputStream;
}
@Override
public PrintWriter getWriter() throws IOException {
if(gZipServletOutputStream != null)
{
throw new IllegalStateException();
}
if(printWriter == null)
{
gZipServletOutputStream = new GZipServletOutputStream(getResponse().getOutputStream());
OutputStreamWriter outputStreamWriter = new OutputStreamWriter(
gZipServletOutputStream,
getResponse().getCharacterEncoding()
);
printWriter = new PrintWriter(outputStreamWriter);
}
return printWriter;
}
@Override
public void setContentLength(int len) {
}
public GZIPOutputStream getGZipOutputStream() {
if(this.gZipServletOutputStream == null)
return null;
return this.gZipServletOutputStream.getGzipOutputStream();
}
}
- 在上例中要注意,由于Servlet规格书中规定,在同一个请求期间,getWriter()与getOutputStream()只能择一调用,否则必须抛出IllegalStateException,因此建议在实现响应封装器时,也遵循这个规范。因此重新定义getOutputStream()与getWriter()方法时,分别要检查是否已存在PrintWriter与ServletOutputStream实例。
- 在getOutputStream()中,会创建GZipServletOutputStream实力并返回。在getWriter()中调用getOutputStream()取得GZipServletOutputStream对象,作为构造PrintWriter实例时使用,这样创建的PrintWriter对象也就具有压缩功能。由于真正的输出会被压缩,忽略原来的内容长度设置。
- 接下来可以实现一个压缩过滤器,使用上面开发的CompressionWrapper来封装原HttpServletResponse。
package chapter5;
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.zip.GZIPOutputStream;
@WebFilter(filterName = "CompressionFilter", urlPatterns = {
"/*"})
public class CompressionFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletResponse resp = (HttpServletResponse)servletResponse;
HttpServletRequest req = (HttpServletRequest)servletRequest;
String encodings = req.getHeader("accept-encoding");
if((encodings != null) && (encodings.indexOf("gzip") > -1))
{
//检查是否接受gzip压缩
CompressionWrapper responseWrapper = new CompressionWrapper(resp);
responseWrapper.setHeader("content-encoding", "gzip");
filterChain.doFilter(servletRequest, responseWrapper);
GZIPOutputStream gzipOutputStream = responseWrapper.getGZipOutputStream();
if(gzipOutputStream != null)
{
gzipOutputStream.finish();
}
}
else
{
filterChain.doFilter(servletRequest, servletResponse);
}
}
@Override
public void destroy() {
}
}
- 浏览器是否接受GZIP压缩格式,可以通过检查accept-encoding请求标头中是否包括gzip字符来判断。如果可以接受GZIP压缩,创建CompressionWrapper封装响应对象,并设置content-encoding响应标头为gzip,这样浏览器就会知道响应内容是GZIP压缩格式。接着调用FilterChain的doFilter()时,传入的响应对象为CompressionWrapper对象。当FilterChain的doFilter()结束时,必须调用GZIPOutputStream的finish()方法,这才会将GZIP后的资料从缓冲区中全部移出并进行响应。
- 如果客户端不接受GZIP压缩格式,则直接调用FilterChain的doFilter(),这样就可以让不接受GZIP压缩格式的客户端也可以收到原有的响应内容。
在这个过滤器真正完成压缩处理之前,Servlet/JSP必须全部清除缓冲的响应资料,否则压缩的数据将是不完整的。
异步处理
- Web容器会为每一个请求分配一个线程,默认情况下,响应完成前,该线程占用的资源都不会被释放。若有些请求需要长时间处理(例如长时间运算、等待某个资源),都会长时间占用线程所需资源,若这类请求很多,许多线程资源都被长时间占用,会对系统性能造成负担。
- Servlet3.0新增了异步处理,可以先释放容器分配给请求的线程和相关资源,减轻系统负担,原先释放了容器所分配线程的请求,其响应将被延后,可以在处理完成(例如长时间运算完成、所需资源已获得)时再对客户端进行相应。
AsyncContext简介
- 为了支持异步处理,在Servlet3.0中,在ServletRequest上提供了startAsync()方法:
AsyncContext startAsync() throws java.lang.IllegalStateException;
AsyncContext startAsync(ServletRequest servletRequest, ServletResponse servletResponse) throws java.lang.IllegalStateException;
- 这两个方法都会返回AsyncContext接口的实现对象,前者会直接利用原有的请求与响应对象来创建AsyncContext,后者可以传入自行创建的请求、响应封装对象。在调用了startAsync()方法取得AsyncContext对象之后,此次请求的响应会被延后,并释放容器分配的线程。
- 可以通过AsyncContext的getRequest()、**getResponse()**方法取得请求、响应对象,此次对客户端的响应将暂缓至调用AsyncContext的complete()或despatch()方法为止,前者表示响应完成,后者表示将调派指定的URL进行响应。
- 若要能调用ServletRequest的startAsync()以取得AsyncContext,必须告知容器此Servlet支持异步处理,如果使用了@WebServlet来标注,则可以设置其asyncSupported为true。
@WebServlet(urlPattern = "/some.do", asyncSupported = true)
- 如果使用web.xml设置Servlet,则可以在<servlet>中设置<async-supported>标签为true:
<servlet>
<servlet-name>AsyncServlet</servlet-name>
<servlet-class>chapter5.AsyncServlet</servlet-class>
<async-supported>true</async-supported>
</servlet>
- 如果Servlet将会进行异步处理,若其前段有过滤器,则过滤器也需要设置支持异步处理,要设置其asyncSupported为true。
package chapter5;
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.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@WebServlet(name = "AsyncServlet", urlPatterns = {
"/async.do"}, asyncSupported = true)
public class AsyncServlet extends HttpServlet {
private ExecutorService executorService = Executors.newFixedThreadPool(10);
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html;charset=UTF-8");
AsyncContext ctx = req.startAsync();
executorService.submit(new AsyncRequest(ctx));
}
@Override
public void destroy() {
executorService.shutdown();
}
}
- 首先告诉容器,这个Servlet支持异步处理,对于每个请求,Servlet会取得其AsyncContext,并释放容器所分配的线程,响应被延后。对于这些被延后响应的请求,创建一个实现Runnable接口的AsyncRequest对象,并将其调度一个线程池(Thread pool),线程池的线程数量是固定的,让这些必须长时间处理的请求,在这些有限数量的线程中完成,而不用每次请求都占用容器分配的线程。
- AsyncRequest是个实现Runnable的类,其模拟了长时间处理。
package chapter5;
import javax.servlet.AsyncContext;
import java.io.PrintWriter;
public class AsyncRequest implements Runnable{
private AsyncContext ctx;
public AsyncRequest(AsyncContext ctx)
{
this.ctx = ctx;
}
@Override
public void run() {
try {
Thread.sleep(10000);
PrintWriter out = ctx.getResponse().getWriter();
out.println("久等了兄弟^_^");
ctx.complete();//对客户端完成响应
}
catch (Exception e)
{
throw new RuntimeException(e);
}
}
}
- 请求与响应都封装在AsyncContext中,所以AsyncRequest建构时必须接受AsyncContext实例。范例中以暂停线程的方式来模拟长时间处理,并输出简单地字符串作为响应文字,最后调用AsyncContext的complete()对客户端完成响应。
模拟服务器推播
- HTTP是基于请求、响应模型,HTTP服务器无法直接对客户端(浏览器)传送信息,因为没有请求就不会有响应。在这种请求响应模型下,如果客户端想要获得服务器端应用程序的最新状态,必须以定期(不定期)方式发送请求,查询服务器端的最新状态。
- 持续发送请求以查询服务器端最新状态,这种方式的问题在于耗用网络流量,如果多次请求过程后,服务器端应用程序状态没有发生变化,那么这多次的请求耗用的流量就是浪费的。
- 一个解决方案是,服务器端将每次请求的响应延后,直到服务器端应用程序状态有变化时再进行响应。当然这样的话,客户端将会处于等待响应状态,如果是浏览器,可以搭配Ajax异步请求技术,而用户将不会因此而被迫停止网页的操作。然而服务器端延后请求的话,如果使用Servlet/JSP技术,等于该请求占用了一个线程,若客户端很多,每个请求都占用线程,将会使得服务器端的性能负担很重。
- Servlet3.0中提供的异步处理技术,可以解决每个请求占用线程的问题,若搭配浏览器端Ajax异步请求技术,就可以达到类似服务器端主动通知浏览器的行为,也就是所谓的服务器端推播(Server push)。
- 以下是实际的例子,模拟应用程序不定期产生最新数据。这个部分由实现ServletContextListener的类负责,会在应用程序启动时进行。
package chapter5;
import javax.servlet.AsyncContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
import java.util.ArrayList;
import java.util.List;
@WebListener()
public class WebInitListener implements ServletContextListener {
private List<AsyncContext> asyncs = new ArrayList<AsyncContext>();
@Override
public void contextInitialized(ServletContextEvent sce) {
sce.getServletContext().setAttribute("asyncs", asyncs);
new Thread(new Runnable() {
@Override
public void run() {
while(true)
{
try {
Thread.sleep((int) (Math.random() * 10000));
double num = Math.random() * 10;
synchronized (asyncs)
{
for(AsyncContext ctx : asyncs)
{
ctx.getResponse().getWriter().println(num);
ctx.complete();
}
asyncs.clear();
}
}
catch (Exception e)
{
throw new RuntimeException(e);
}
}
}
}).start();
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
}
}
- 在这个ServletContextListener中,有个List会存储所有异步请求的AsyncContext,并在不定时产生数字后,逐一对客户端进行响应,并调用AsyncContext的complete()来完成请求。
- 负责接受请求的Servlet一旦收到请求,就将之加入到List中。
package chapter5;
import javax.servlet.AsyncContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebListener;
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.List;
@WebServlet(name = "AsyncNumServlet", urlPatterns = {
"/asyncNum.do"}, asyncSupported = true)
public class AsyncNumServlet extends HttpServlet {
private List<AsyncContext> asyncs;
@Override
public void init() throws ServletException {
asyncs = (List<AsyncContext>) getServletContext().getAttribute("asyncs");
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
AsyncContext ctx = req.startAsync();//开始异步处理
synchronized (asyncs)
{
asyncs.add(ctx);//加入维护AsyncContext的List中
}
}
}
- 由于维护AsyncContext的List是存储为ServletContext属性,所以在这个Servlet中,必须从ServletContext中取出,在每次请求来到时,调用HttpServletRequest的startAsync()进行异步处理,并将取得AsyncContext加入至维护AsyncContext的List中。
- 可以使用一个简单的HTML,其中使用Ajax技术,发送异步请求至服务器端,这个请求会被延迟,直到服务器端完成响应之后,更新网页上对应的资料,并再度发送异步请求。
<!DOCTYPE html>
<html lang="en">
<head>
<title>实时资料</title>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<script type="text/javascript">
function asyncUpdate(){
if (window.XMLHttpRequest){
xhr = new XMLHttpRequest();
}
else if(window.activeXObject){
xhr = new ActiveXObject('Microsoft.XMLHTTP');
}
xhr.onreadystatechange = function (){
if(xhr.readyState === 4){
if(xhr.status === 200){
document.getElementById('data').innerHTML = xhr.responseText;
asyncUpdate();
}
}
};
xhr.open('GET', 'asyncNum.do?timestamp=' + new Date().getTime());
xhr.send(null);
}
window.onload = asyncUpdate;
</script>
</head>
<body>
实时资料:<span id="data">0</span>
</body>
</html>
- 可以试着使用多个浏览器视窗来请求这个页面,会看到每个浏览器视窗的资料都是同步的。
更多AsyncContext细节
- 如果Servlet或过滤器的asyncSupported被标示为true,则它们支持异步请求处理,在不支持异步处理的Servlet或过滤器中调用startAsync(),会抛出IllegalStateException。
- 当在支持异步处理的Servlet或过滤器中调用请求对象的startAsync()方法时,该次请求会离开容器所分配的线程,这意味着必须响应处理流程会返回,也就是若有过滤器,也会依序返回(各自完成FilterChain的doFilter()方法),但最终的响应被延迟。
- 可以调用AsyncContext的complete()方法完成响应,而后调用forward()方法,将响应转发给别的Servlet/JSP处理。
- 不可以在两个异步处理的Servlet间派送前,连续调用两次startAsync(),否则会抛出IllegalStateException。
- 将请求从支持异步处理的Servlet派送至一个同步处理的Servlet是可行的,此时,容器会负责调用AsyncContext的complete()。
- 如果从一个同步处理的Servlet派送至一个支持异步处理的Servlet,在异步处理的Servlet中调用AsyncContext的startAsync(),将会抛出IllegalStateException。
- 如果对AsyncContext的起始、完成、超时或错误发生等事件有兴趣,可以实现AsyncListener。
public interface AsyncListener extends EventListener{
void onComplete(AsyncEvent event)throws IOException;
void onTimeout(AsyncEvent event)throws IOException;
void onError(AsyncEvent event)throws IOException;
void onStartAsync(AsyncEvent event)throws IOException;
}