第9章 Servlet高级


学习目标

Servlet规范有三个高级特性,分别是Filter、Listener和文件的上传下载。

1. Filter

1.1 什么是Filter

在Servlet高级特性中,Filter被称为过滤器,Filter基本功能就是对Servlet容器调用Servlet的过程进行拦截,它位于客户端和处理程序之间,能够对请求和响应进行检查和修改

图1 Filter在Web应用中的拦截过程

Filter在Web应用中的拦截过程,当客户端对服务器资源发出请求时,服务器会根据过滤规则进行检查,如果客户的请求满足过滤规则,则对客户请求进行拦截,对请求头和请求数据进行检查或修改,并依次通过过滤器链,最后把过滤之后的请求交给处理程序,处理程序处理后再将响应依次逆向通过过滤器对响应进行处理。请求信息在过滤器链中可以被修改,也可以根据客户端的请求条件不将请求发往处理程序。

图2 Filter应用

Filter除了可以实现拦截功能,还可以提高程序的性能,在Web开发时,不同的Web资源中的过滤操作可以放在同一个Filter中完成,这样可以不用多次编写重复代码,从而提高了程序的性能。

1.2 Filter相关API

Filter接口

方法声明 功能描述
init(FilterConfig filterConfig) init()方法是Filter的初始化方法,创建Filter实例后将调用init()方法。该方法的参数filterConfig用于读取Filter的初始化参数。
doFilter(ServletRequest request,ServletResponse response,FilterChain chain) doFilter()方法完成实际的过滤操作,当客户的请求满足过滤规则时,Servlet容器将调用过滤器的doFilter()方法完成实际的过滤操作。doFilter()方法有多个参数,其中,参数request和response为Web服务器或Filter链中的上一个Filter传递过来的请求和响应对象;参数chain代表当前Filter链的对象。
destroy() 该方法用于释放被Filter对象打开的资源,例如关闭数据库和 IO流。destroy()方法在Web服务器释放Filter对象之前被调用。

FilterConfig接口

方法声明 功能描述
String getFilterName() 返回Filter的名称
ServletContext getServletContext() 返回FilterConfig对象中封装的ServletContext对象
String getInitParameter(String name) 返回名为name的初始化参数值
Enumeration getInitParameterNames() 返回Filter所有初始化参数的枚举

FilterChain接口

1.3 Filter的生命周期

Filter的生命周期可分为创建、执行、销毁三个阶段。

1.4 实现第一个Filter

参照ch04创建ch09_xj项目,创建ch09首页index.jsp

<%@ page contentType="text/html;charset=UTF-8"%>
<html>
  <head>
    <title>Servlet高级</title>
  </head>
  <body>
  <h1>ch09 Servlet高级(许杰)</h1>
  <hr>
  <a href="01myServlet">1. 实现第一个Filter</a><br>
  <a href="02forwardServlet">2. Filter映射</a><br>
  <a href="03myServlet">3. Filter链</a><br>
  <a href="04/index.jsp">4. Filter在Cookie自动登录中的使用</a><br>
  <a href="05myjsp.jsp">5. 监听域对象的生命周期</a><br>
  <a href="06FileUpForm.jsp">6. 文件上传(commons-fileupload/commons-io)</a><br>
  <a href="07FileUpForm.html">7. 文件上传(使用javax.servlet.http.Part)</a><br>
  <a href="08download.jsp">8. 下载</a><br>
  <a href="09/index_xj.jsp">9. 首页(09/index_xj.jsp)访问次数统计</a><br>
  <a href="10index_xj.jsp">10. 图片资源防盗过滤器(保护/images/下资源)</a><br>
  <a href="11/index.jsp">11. Login过滤器(只有登录的用户才能访问/ch09_xj/11/目录及其子目录的资源)</a><br>
  <a href="12/index.jsp">12. 敏感文字过滤(过滤:糟糕,混蛋,他妈的,他奶奶的)</a><br>
  <a href="13/index.jsp">13. 登录(使用监听查看在线用户)</a> <a href="13/showUser.jsp">查看在线用户</a><br>
  <hr>
  <p><a href="http://101.42.158.247/21javaweb.html">返回课程首页</a>
    许杰 <script>document.write(document.lastModified); </script> 制作</p>
  </body>
</html>

运行结果如下:

图3 ch09 Servlet高级--首页

cn.xj.servlet.MyServlet.java

package cn.xj.servlet;

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

@WebServlet("/01myServlet")
public class MyServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("text/html;charset=utf-8");
        resp.getWriter().write("--------Hello MyServlet. 嗨,许杰。<br>");
    }
}

运行结果如下:

图4 没有过滤器时执行的结果

cn.xj.filter.MyFilter.java

package cn.xj.filter;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;
import java.io.PrintWriter;

@WebFilter("/01myServlet")
public class MyFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("MyFilter 初始化...应用程序启动时初始化");
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        servletResponse.setContentType("text/html;charset=utf-8");
        PrintWriter out=servletResponse.getWriter();
        out.write("Hello MyFilter. 许杰的过滤器。<br>");
        out.write("--------- filterChain.doFilter 之前: 请求预处理 ----------<br>");
        filterChain.doFilter(servletRequest, servletResponse);
        out.write("--------- filterChain.doFilter 之后:响应后处理 -----------<br>");
     }

    @Override
    public void destroy() {
        System.out.println("MyFilter 销毁... 应用程序停止时销毁");
    }
}

运行结果如下:

图5 添加过滤器后执行的结果

@WebFilter注解的常用属性

属性名 类型 描述
filterName String 指定过滤器的名称。默认是过滤器类的名称。
urlPatterns String[] 指定一组过滤器的URL匹配模式。
value String[] 该属性等价于urlPatterns属性。urlPatterns和value属性不能同时使用。
servletNames String[] 指定过滤器将应用于哪些Servlet。取值是 @WebServlet 中的 name 属性的取值。
dispatcherTypes DispatcherType 指定过滤器的转发模式。具体取值包括:ERROR、FORWARD、INCLUDE、REQUEST。
initParams WebInitParam[] 指定过滤器的一组初始化参数。

1.5 Filter映射

Filter的映射方式可分为两种:

@WebFilter注解有一个特殊的属性dispatcherTypes,它可以指定过滤器的转发模式,dispatcherTypes属性有4个常用值:REQUEST、INCLUDE、FORWARD和ERROR。

cn/xj/servlet/ForwardServlet.java

package cn.xj.servlet;

import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.annotation.WebServlet;

@WebServlet("/02forwardServlet")
public class ForwardServlet extends HttpServlet {
    public void doGet(HttpServletRequest request,
                      HttpServletResponse response)
            throws ServletException, IOException {
        request.getRequestDispatcher("/02first.jsp").forward(request, response);
    }

    public void doPost(HttpServletRequest request,
                       HttpServletResponse response)
            throws ServletException, IOException {
        doGet(request, response);
    }
}

cn/xj/filter/ForwardFilter.java

package cn.xj.filter;
import java.io.*;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
/*@WebFilter("/02first.jsp")*/
@WebFilter(urlPatterns = "/02first.jsp", dispatcherTypes = DispatcherType.FORWARD)
public class ForwardFilter implements Filter {
    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain) throws IOException, ServletException {
        // 用于拦截用户的请求,如果和当前过滤器的拦截路径匹配,该方法会被调用
        response.setContentType("text/html;charset=utf-8");
        PrintWriter out=response.getWriter();
        out.write("Hello FilterTest. ForwardFilter过滤器。");
    }
}

web/02first.jsp

<%@ page contentType="text/html; charset=utf-8"
         pageEncoding="utf-8"%>
<html>
<head>
    <title></title>
</head>
<body>
    first.jsp
</body>
</html>

运行结果如下:

图6 Filter映射

1.6 Filter链

图7 Filter链流程示意

cn.xj.servlet.MyServlet03.java

package cn.xj.servlet;

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

@WebServlet("/03myServlet")
public class MyServlet03 extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("text/html;charset=utf-8");
        resp.getWriter().println("Hello MyServlet03. 嗨,许杰。<br>");
    }
}

cn.xj.filter.MyFilter01.java

package cn.xj.filter;
import java.io.*;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
@WebFilter("/03myServlet")
public class MyFilter01 implements Filter {
    public void init(FilterConfig fConfig) throws ServletException {
        // 过滤器对象在初始化时调用,可以配置一些初始化参数
    }
    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain) throws IOException, ServletException {
        // 用于拦截用户的请求,如果和当前过滤器的拦截路径匹配,该方法会被调用
        response.setContentType("text/html;charset=utf-8");
        PrintWriter out=response.getWriter();
        out.println("MyFilter01 之前 许杰<br>");
        chain.doFilter(request, response);
        out.println("MyFilter01 之后 许杰<br>");
    }
    public void destroy() {
        // 过滤器对象在销毁时自动调用,释放资源
    }
}

cn.xj.filter.MyFilter02.java

package cn.lp.filter;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;
import java.io.PrintWriter;

@WebFilter("/03myServlet")
public class MyFilter02 implements Filter {

    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain) throws IOException, ServletException {
        // 用于拦截用户的请求,如果和当前过滤器的拦截路径匹配,该方法会被调用
        response.setContentType("text/html;charset=utf-8");
        PrintWriter out=response.getWriter();
        out.println("MyFilter02 Before 之前<br>");
        chain.doFilter(request, response);
        out.println("MyFilter02 After 之后<br>");
    }

}

运行结果如下:

图8 Filter链

要实现Filter对Cookie进行拦截并实现自动登录,需要有以下几个文件。

(1) cn/xj/entity/User.java

package cn.xj.entity;
public class User {
    private String 用户名;
    private String 密码;

    // getter | setter
}

(2) web/04/login.jsp

<%@ page contentType="text/html; charset=utf-8" %>
<html>
<head>
    <title>用户登录</title>
</head>
<body style="text-align: center;">
    <center><h3>用户登录</h3></center>
    <form action="${pageContext.request.contextPath }/04/loginServlet"
          method="post">
        <table border="1" width="600px" cellpadding="0" cellspacing="0"
               align="center" >
            <tr>
                <td height="30" align="center">用户名:</td>
                <td>&nbsp;&nbsp;
                    <input type="text" name="用户名" />${errerMsg }</td>
            </tr>
            <tr>
                <td height="30" align="center">&nbsp; 码:</td>
                <td>&nbsp;&nbsp;
                    <input type="password" name="密码" /></td>
            </tr>
            <tr>
                <td height="35" align="center">自动登录时间</td>
                <td><input type="radio" name="autologin"
                           value="${60*60*24*31}"/>一个月
                    <input type="radio" name="autologin"
                           value="${60*60*24*31*3}"/>三个月
                    <input type="radio" name="autologin"
                           value="${60*60*24*31*6}"/>半年
                    <input type="radio" name="autologin"
                           value="${60*60*24*31*12}"/>一年
                </td>
            </tr>
            <tr>
                <td height="30" colspan="2" align="center">
                    <input type="submit" value="登录" />
                    &nbsp;&nbsp;&nbsp;&nbsp;
                    <input type="reset" value="重置" />
                </td>
            </tr>
        </table>
    </form>
</body>
</html>

(3) cn/xj/servlet/LoginServlet.java

package cn.xj.servlet;
import java.io.IOException;
import java.net.URLEncoder;
import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.*;
import cn.xj.entity.User;
@WebServlet("/04/loginServlet")
public class LoginServlet extends HttpServlet {
    public void doGet(HttpServletRequest request,
                      HttpServletResponse response)
            throws ServletException, IOException {
        // 获得用户名和密码
        request.setCharacterEncoding("utf-8");
        String username = request.getParameter("用户名");
        String password = request.getParameter("密码");
        // 检查用户名和密码
        if ("许杰".equals(username) && "123456".equals(password)) {
            // 登录成功
            // 将用户状态 user 对象存入 session域
            User user = new User();
            user.set用户名(username);
            user.set密码(password);
            request.getSession().setAttribute("user", user);
            // 发送自动登录的cookie
            String autoLogin = request.getParameter("autologin");
            if (autoLogin != null) {
                // 注意 cookie 中的密码要加密
                Cookie cookie = new Cookie("autologin", URLEncoder.encode( username,"utf-8") + "-"
                        + password);
                cookie.setMaxAge(Integer.parseInt(autoLogin));
                cookie.setPath(request.getContextPath()+"/04");
                response.addCookie(cookie);
            }
            // 跳转至首页
            response.sendRedirect(request.getContextPath()+"/04/index.jsp");
        } else {
            request.setAttribute("errerMsg", "用户名或密码错误");
            request.getRequestDispatcher("/04/login.jsp")
                    .forward(request,response);
        }
    }
    public void doPost(HttpServletRequest request,
                       HttpServletResponse response)
            throws ServletException, IOException {
        doGet(request, response);
    }
}

(4) web/04/index.jsp

<%@ page contentType="text/html; charset=utf-8" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<html>
<head>
  <title>显示登录的用户信息</title>
</head>
<body>
  <br>
  <center>
    <h3>欢迎光临</h3>
  </center>

  <c:choose>
    <c:when test="${sessionScope.user==null }">
      <a href="${pageContext.request.contextPath }/04/login.jsp">用户登录</a>
    </c:when>
    <c:otherwise>
      欢迎你,${sessionScope.user.用户名 }!
      <a href="${pageContext.request.contextPath }/04/logoutServlet">注销</a>
    </c:otherwise>
  </c:choose>
  <hr>
</body>
</html>

(5) cn/xj/filter/AutoLoginFilter.java

@WebFilter("/04/*") ---- 对 /04/目录下所有的web资源都执行此过滤器

package cn.xj.filter;
import java.io.IOException;
import java.net.URLDecoder;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.*;
import cn.xj.entity.User;
@WebFilter("/04/*")
public class AutoLoginFilter implements Filter {
    public void doFilter(ServletRequest req, ServletResponse response,
                         FilterChain chain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        // 获得一个名为 autologin 的cookie
        Cookie[] cookies = request.getCookies();
        String autologin = null;
        for (int i = 0; cookies != null && i < cookies.length; i++) {
            if ("autologin".equals(cookies[i].getName())) {
                // 找到了指定的cookie
                autologin = cookies[i].getValue();
                break;
            }
        }
        if (autologin != null) {
            // 做自动登录
            String[] parts = autologin.split("-");
            String username = URLDecoder.decode(parts[0],"utf-8");
            String password = parts[1];
            // 检查用户名和密码
            if ("许杰".equals(username)&& ("123456").equals(password)) {
                // 登录成功,将用户状态 user 对象存入 session域
                User user = new User();
                user.set用户名(username);
                user.set密码(password);
                request.getSession().setAttribute("user", user);
            }
        }
        // 放行
        chain.doFilter(request, response);
    }
}

(6) cn.xj.servlet.LogoutServlet.java

package cn.xj.servlet;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebServlet("/04/logoutServlet")
public class LogoutServlet extends HttpServlet {
    public void doGet(HttpServletRequest request,
                      HttpServletResponse response)
            throws ServletException, IOException {
        // 用户注销
        request.getSession().removeAttribute("user");
        // 从客户端删除自动登录的cookie
        Cookie cookie = new Cookie("autologin", "msg");
        cookie.setPath(request.getContextPath()+"/04");
        cookie.setMaxAge(0);
        response.addCookie(cookie);
        response.sendRedirect(request.getContextPath()+"/04/index.jsp");
    }
    public void doPost(HttpServletRequest request,
                       HttpServletResponse response)
            throws ServletException, IOException {
        doGet(request, response);
    }
}

运行结果如下:

图9 Filter在Cookie自动登录中的使用

2. Listener

2.1 Listener概述

在Web程序开发中,经常需要对某些事件进行监听,以便及时作出处理,如监听鼠标单击事件、监听键盘按下事件等。为此,Servlet提供了监听器(Listener),专门用于监听Servlet事件。

Listener的重要组成部分

Listener在监听过程中会涉及几个重要组成部分,具体如下。

注意:当用户执行一个操作触发事件源上的事件时,该事件会被事件监听器监听到,当监听器监听到事件发生时,相应的事件处理器就会对发生的事件进行处理。

事件监听器的工作过程

2.2 Listener的API

Listener中8个不同的监听器接口

类型 描述
ServletContextListener 用于监听ServletContext对象的创建与销毁过程
HttpSessionListener 用于监听HttpSession对象的创建和销毁过程
ServletRequestListener 用于监听ServletRequest对象的创建和销毁过程
ServletContextAttributeListener 用于监听ServletContext对象中的属性变更
HttpSessionAttributeListener 用于监听HttpSession对象中的属性变更
ServletRequestAttributeListener 用于监听ServletRequest对象中的属性变更
HttpSessionBindingListener 用于监听JavaBean对象绑定到HttpSession对象和从HttpSession对象解绑的事件
HttpSessionActivationListener 用于监听HttpSession中对象活化和钝化的过程

Listener中的8种Servlet事件监听器可以分为三类,具体如下。

Servlet3.1中web对象、监听器接口与其事件的对应关系
Web对象 Listener接口 Event(事件) 方法
application(应用)
或(ServletContext上下文)
ServletContextListener(Servlet上下文监听器) ServletContextEvent(生命周期事件) contextInitialized(ServletContextEvent arg0)--初始化
contextDestroyed(ServletContextEvent arg0)--销毁
ServletContextAttributeListener(Servlet上下文属性监听器) ServletContextAttributeEvent(属性改变事件) attributeAdded(ServletContextAttributeEvent arg0)--增加后
attributeRemoved(ServletContextAttributeEvent event)--移除后
attributeReplaced(ServletContextAttributeEvent event)--替换
session(会话)
或(HttpSession Http会话)
HttpSessionListener(Http会话监听器) HttpSessionEvent(生命周期事件) sessionCreated(HttpSessionEvent se)--创建后
sessionDestroyed(HttpSessionEvent se)--销毁后
HttpSessionAttributeListener(Http会话属性监听器) HttpSessionBindingEvent(属性改变事件) attributeAdded(HttpSessionBindingEvent event)--增加后
attributeRemoved(HttpSessionBindingEvent event)--移除后
attributeReplaced(HttpSessionBindingEvent event)--替换
HttpSessionActivationListener(http会话激活监听器) HttpSessionEvent(会话迁移事件) sessionDidActivate(HttpSessionEvent se)--会话激活
sessionWillPassivate(HttpSessionEvent se)--会话钝化
HttpSessionBindingListener(http会话激活监听器) HttpSessionBindingEvent(对象绑定事件) valueBound(HttpSessionBindingEvent event)--绑定
valueUnbound(HttpSessionBindingEvent event)--解绑
HttpSessionIdListener(http会话ID改变监听器) HttpSessionEvent(会话迁移事件) sessionDidActivate(HttpSessionEvent se)--会话激活
sessionWillPassivate(HttpSessionEvent se)--会话钝化
request(请求)
或(ServletRequest Servlet请求)
ServletRequestListener(Servlet请求监听器) ServletRequestEvent(生命周期事件) requestInitialized(ServletRequestEvent arg0)--请求初始化
requestDestroyed(ServletRequestEvent arg0)--请求销毁
ServletRequestAttributeListener(Servlet请求属性监听器) ServletRequestAttributeEvent(请求属性改变事件) attributeAdded(ServletRequestAttributeEvent arg0)--增加后
attributeRemoved(ServletRequestAttributeEvent arg0)--移除后
attributeReplaced(ServletRequestAttributeEvent arg0)--替换
AsyncListener(异步请求监听器) AsyncEvent(异步请求事件) onStartAsync(AsyncEvent arg0)--异步开始
onComplete(AsyncEvent arg0)--异步完成
onTimeout(AsyncEvent arg0)--异步超时
onError(AsyncEvent arg0)--异步出错
图10 Listener监听器

任务:监听域对象的生命周期

(1) cn/xj/listener/MyListener.java

package cn.xj.listener;
import javax.servlet.*;
import javax.servlet.annotation.WebListener;
import javax.servlet.http.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

//@WebListener
public class MyListener implements
        ServletContextListener, HttpSessionListener,ServletRequestListener {
    DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(" yyyy-MM-dd HH:mm:ss ");
    public void contextInitialized(ServletContextEvent arg0) {
        System.out.println("ServletContext被创建了【杜】"+ LocalDateTime.now().format(dateTimeFormatter)+arg0.getServletContext().getContextPath());
    }
    public void contextDestroyed(ServletContextEvent arg0) {
        System.out.println("ServletContext被销毁了【杜】"+ LocalDateTime.now().format(dateTimeFormatter)+arg0.getServletContext().getContextPath());
    }
    public void requestInitialized(ServletRequestEvent arg0) {
        System.out.println("    ServletRequest被创建了【杜】"+ LocalDateTime.now().format(dateTimeFormatter)+arg0.getServletRequest().getRemoteAddr());
    }
    public void requestDestroyed(ServletRequestEvent arg0) {
        System.out.println("    ServletRequest被销毁了【杜】"+ LocalDateTime.now().format(dateTimeFormatter)+arg0.getServletRequest().getRemoteAddr());
    }
    public void sessionCreated(HttpSessionEvent arg0) {
        System.out.println("  HttpSession被创建了【杜】"+ LocalDateTime.now().format(dateTimeFormatter)+arg0.getSession().getId());
    }
    public void sessionDestroyed(HttpSessionEvent arg0) {
        System.out.println("  HttpSession被销毁了【杜】"+ LocalDateTime.now().format(dateTimeFormatter)+arg0.getSession().getId());
    }
}

(2) web/05myjsp.jsp

<%@ page contentType="text/html; charset=utf-8" pageEncoding="utf-8"%>
<html>
<head>
    <title>this is MyJsp.jsp page</title>
</head>
<body>
    这是一个测试监听器的页面
    <p>//@WebListener 运行时放开此句;在控制台截图;测试万屏蔽此句。</p>
    <%
        System.out.println("      这是一个测试监听器的页面【许杰】。");
    %>
</body>
</html>

(3) web/WEB-INF/web.xml

    <session-config>
        <session-timeout>1</session-timeout>
    </session-config>

(4) 运行测试

开启ch09_xj应用程序、3次单击测试网页、等待几分钟、停止ch09_xj应用程序):

图11 监听域对象的生命周期

已连接到服务器
[2022-10-06 12:32:53,521] 工件 ch09_xj: 正在部署工件,请稍候…
ServletContext被创建了【杜】 2022-10-06 00:32:54 /ch09_xj
...
    ServletRequest被创建了【杜】 2022-10-06 00:32:54 127.0.0.1
  HttpSession被创建了【杜】 2022-10-06 00:32:55 9323175E2E7F95D2703299EAFFBF09BD
    ServletRequest被销毁了【杜】 2022-10-06 00:32:55 127.0.0.1
    ServletRequest被创建了【杜】 2022-10-06 00:32:55 127.0.0.1
  HttpSession被创建了【杜】 2022-10-06 00:32:55 0965BC39BA900B491CAA293F070880A2
    ServletRequest被销毁了【杜】 2022-10-06 00:32:55 127.0.0.1
    ServletRequest被创建了【杜】 2022-10-06 00:33:02 127.0.0.1
      这是一个测试监听器的页面【许杰】。
    ServletRequest被销毁了【杜】 2022-10-06 00:33:03 127.0.0.1
...
    ServletRequest被创建了【杜】 2022-10-06 00:33:10 127.0.0.1
      这是一个测试监听器的页面【许杰】。
    ServletRequest被销毁了【杜】 2022-10-06 00:33:10 127.0.0.1
  HttpSession被销毁了【杜】 2022-10-06 00:34:53 9323175E2E7F95D2703299EAFFBF09BD
  HttpSession被销毁了【杜】 2022-10-06 00:34:53 0965BC39BA900B491CAA293F070880A2
    ServletRequest被创建了【杜】 2022-10-06 00:35:34 127.0.0.1
  HttpSession被创建了【杜】 2022-10-06 00:35:34 730CB686A570F522C77C08DA3C745B36
      这是一个测试监听器的页面【许杰】。
    ServletRequest被销毁了【杜】 2022-10-06 00:35:34 127.0.0.1
  HttpSession被销毁了【杜】 2022-10-06 00:36:53 730CB686A570F522C77C08DA3C745B36
C:\apache-tomcat-9.0.65\bin\catalina.bat stop
...
ServletContext被销毁了【杜】 2022-10-06 00:37:04 /ch09_xj
...
已与服务器断开连接

(5) 测试后善后

注意:测试完本题后web.xml屏蔽到有关session超时的设置、MyListener中的监听器注解,以防引起其他的应用程序的运行激活本监听器的运行。代码如下。

<!--    <session-config>
        <session-timeout>1</session-timeout>
    </session-config>-->

//@WebListener
public class MyListener implements

3. Servlet 3.0新特性

web.xml配置 --新增-->注解配置、同步Servlet执行流程 --新增--> 异步Servlet执行流程

(1) Servlet 3.0注解新特性

Servlet 3.0的新特性,例如,已经使用过的@WebServlet注解、@WebFilter注解,注解就是Servlet 3.0的新特性之一,通过使用注解的方式简化了Servlet的配置。在Servlet 3.0之前,web框架需要在web.xml中配置。在Servlet 3.0之后,可以用注解的方式配置web框架,简化了web框架的开发。

(2) Servlet 3.0常见的注解:

(3) 普通Servlet的工作流程

在Servlet 3.0之前,一个普通Servlet的工作流程大致如下。

在上述的Servlet工作流程的第(2)步是业务处理通常是最耗时的,这主要体现在数据库操作,以及其他的跨网络调用等。在此过程中,Servlet线程一直处于阻塞状态,直到业务方法执行完毕。在处理业务的过程中,Servlet资源一直被占用而得不到释放,对于并发较大的应用,可能造成性能瓶颈。对于这个问题,Servlet 3.0之前,通常是采用提前结束Servlet线程的方式,及时释放资源。

(4) 异步处理的Servlet工作流程

Servlet 3.0的异步处理特性可以提高Web程序的接口处理速度。

Servlet 3.0通过异步处理,将之前的Servlet工作流程进行了调整,具体如下。

注意:如此一来,Servlet线程不再是一直处于阻塞状态等待业务逻辑处理完成,而是启动异步线程之后可以立即返回。

(5) Web.xml配置开启异步处理

对于使用web.xml文件配置Servlet和过滤器的情况,Servlet 3.0在Servlet标签中增加了async-supported子标签,该标签的默认取值为false,要启用异步处理支持,将其设置为 true 即可。

<servlet>
    <servlet-name>MyServlet</servlet-name>
    <servlet-class>cn.itcast.chapter09.filter.MyServlet</servlet-class>
    <async-supported>true</async-supported>
</servlet>

(6) 注解配置开启异步处理

对于使用Servlet 3.0提供的@WebServlet和@WebFilter注解对 Servlet 或过滤器进行配置的情况,由于这两个注解都提供了asyncSupported属性,因此可以通过设置asyncSupported属性值开启异步处理。asyncSupported默认值为false,要启用异步处理支持,只需将该属性设置为true即可。

@WebFilter(filterName="MyFilter",urlPatterns="/MyServlet",asyncSupported=true)

4. 文件的上传和下载

4.1 文件上传的原理

文件上传

要实现Web开发中的文件上传功能,通常需完成两步操作:一是在Web项目的页面中添加上传输入项,二是在Servlet中读取上传文件的数据,并保存到目标路径中。 由于大多数文件的上传都是通过表单的形式提交给服务器的,因此,要想在程序中实现文件上传功能,首先要创建一个用于提交上传文件的表单页面。在表单页面中,需要使用标签在jsp页面中添加文件上传输入项。

<input type="file">标签

<input type="file">标签的使用需要注意以下两点:

<%--指定表单数据的enctype属性以及提交方式--%>
<form enctype="multipart/form-data" method="post">
    <%--指定标记的类型和文件域的名称--%>
    选择上传文件:<input type="file" name="myfile"/><br />
</form>

4.2 认识Commons-FileUpload组件

Commons-FileUpload组件

当浏览器通过表单提交上传文件时,文件数据都附带在HTTP请求消息体中,并且采用MIME类型(多用途互联网邮件扩展类型)进行描述,在后台可以使用request对象提供的getInputStream()方法读取客户端提交过来的数据。但由于用户可能会同时上传多个文件,而在Servlet端直接读取上传数据,并分别解析出相应的文件数据是一项非常麻烦的工作。为了方便处理用户上传的数据,Apache组织提供了一个开源组件Commons-FileUpload,该组件可以方便地将“multipart/form-data”类型请求中的各种表单域解析出来,并实现一个或多个文件的上传,同时也可以限制上传文件的大小等。Commons-FileUpload组件性能十分优异,并且使用非常简单。

图12 Commons-FileUpload组件

需要注意的是,在使用Commons-FileUpload组件时,需要导入commons-fileupload.jar和commons-io.jar两个JAR包,这两个JAR包可以在Apache官网下载。

http://archive.apache.org/dist/commons/fileupload/binaries
commons-fileupload-1.4-bin.zip
commons-fileupload-1.4.jar

https://commons.apache.org/proper/commons-io/download_io.cgi
commons-io-2.11.0-bin.zip
commons-io-2.11.jar

FileItem接口

FileItem接口主要用于封装单个表单字段元素的数据,一个表单字段元素对应一个FileItem对象。Commons-FileUpload组件在处理文件上传的过程中,将每一个表单域(包括普通的文本表单域和文件域)封装在一个FileItem对象中

为了便于讲解,在此将FileItem接口的实现类称为FileItem类,FileItem类实现了序列化接口Serializable,因此,FileItem类支持序列化操作。

FileItem类的方法

方法声明 功能描述
boolean isFormField() isFormField()方法用于判断FileItem类对象封装的数据是一个普通文本表单字段,还是一个文件表单字段,如果是普通文本表单字段则返回true,否则返回false。
String getName() getName()方法用于获取文件上传字段中的文件名。如果FileItem类对象对应的是普通文本表单字段,getName()方法将返回null,否则,只要浏览器将文件的字段信息传递给服务器,getName()方法就会返回一个字符串类型的结果,如C:\Sunset.jpg。
String getFieldName() getFieldName()方法用于获取表单字段元素描述头的name属性值,也是表单标签name属性的值。例如“name=file1”中的“file1”。
void write(File file) write()方法用于将FileItem对象中保存的主体内容保存到某个指定的文件中。如果FileItem对象中的主体内容是保存在某个临时文件中,那么该方法顺利完成后,临时文件有可能会被清除。另外,该方法也可将普通表单字段内容写入到一个文件中,但它主要用于将上传的文件内容保存到本地文件系统中。
String getString() getString()方法用于将FileItem对象中保存的数据流内容以一个字符串形式返回。它有两个重载的定义形式:①public String getString()②public String getString(java.lang.String encoding)前者使用默认的字符集编码将主体内容转换成字符串,后者使用参数指定的字符集编码将主体内容转换成字符串。
String getContentType() getContentType()方法用于获得上传文件的类型,即表单字段元素描述头属性“Content-Type”的值,如“image/jpeg”。如果FileItem类对象对应的是普通表单字段,该方法将返回null。

DiskFileItemFactory类

DiskFileItemFactory类用于将请求消息实体中的每一个文件封装成单独的FileItem对象。如果上传的文件比较小,将直接保存在内存中,如果上传的文件比较大,则会以临时文件的形式,保存在磁盘的临时文件夹中。默认情况下,不管文件保存在内存还是磁盘临时文件夹,文件存储的临界值是10240字节,即10KB。

DiskFileItemFactory类的构造方法

方法声明 功能描述
DiskFileItemFactory() 采用默认临界值和系统临时文件夹构造文件项工厂对象
DiskFileItemFactory(int sizeThreshold,File repository) 采用参数指定临界值和系统临时文件夹构造文件项工厂对象

上表中列举了DiskFileItemFactory类的两个构造方法,其中,第二个构造方法需要传递两个参数,第一个参数sizeThreshold表示文件保存在内存还是磁盘临时文件夹中的临界值,第二个参数repository表示临时文件的存储路径。

ServletFileUpload类

ServletFileUpload类是Apache组件处理文件上传的核心高级类,通过调用parseRequest(HttpServletRequest) 方法可以将HTML中每个表单提交的数据封装成一个FileItem对象,然后以List列表的形式返回。

ServletFileUpload类的构造方法

方法声明 功能描述
ServletFileUpload() 构造一个未初始化的ServletFileUpload实例对象
ServletFileUpload(FileItemFactory fileItemFactory) 根据参数指定的FileItemFactory 对象创建一个ServletFileUpload对象

上表中列举了ServletFileUpload类的两个构造方法。在文件上传过程中,在使用第一个构造方法创建ServletFileUpload对象时,需要在解析请求之前调用setFileItemFactory()方法设置fileItemFactory属性。

4.3 动手实践:实现文件上传

将commons-fileupload-1.4.jar和commons-io-2.11.jar复制到项目后还需要在“Moxjles”中配置jar包。

(1) cn/xj/servlet/UploadServlet.java

package cn.xj.servlet;
import java.io.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.annotation.MultipartConfig;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.*;
import org.apache.commons.fileupload.*;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
//上传文件的Servlet类
@WebServlet("/06uploadServlet")
//该注解用于标注文件上传的Servlet
@MultipartConfig
public class UploadServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;
    public void doGet(HttpServletRequest request,
                      HttpServletResponse response)throws ServletException, IOException {
        try {
            //设置ContentType字段值
            response.setContentType("text/html;charset=utf-8");
            // 创建DiskFileItemFactory工厂对象
            DiskFileItemFactory factory = new DiskFileItemFactory();
            //设置文件缓存目录,如果该目录不存在则新创建一个
            File f = new File("E:\\TempFolder");
            if (!f.exists()) {
                f.mkdirs();
            }
            // 设置文件的缓存路径
            factory.setRepository(f);
            // 创建 ServletFileUpload对象
            ServletFileUpload fileupload = new ServletFileUpload(factory);
            //设置字符编码
            fileupload.setHeaderEncoding("utf-8");
            // 解析 request,得到上传文件的FileItem对象
            List<FileItem> fileitems = fileupload.parseRequest(request);
            //获取字符流
            PrintWriter writer = response.getWriter();
            // 遍历集合
            for (FileItem fileitem : fileitems) {
                // 判断是否为普通字段
                if (fileitem.isFormField()) {
                    // 获得字段名和字段值
                    String name = fileitem.getFieldName();
                    if(name.equals("name")){
                        //如果文件不为空,将其保存在value中
                        if(!fileitem.getString().equals("")){
                            String value = fileitem.getString("utf-8");
                            writer.print("上传者:" + value + "<br />");
                        }
                    }
                } else {
                    // 获取上传的文件名
                    String filename = fileitem.getName();
                    //处理上传文件
                    if(filename != null && !filename.equals("")){
                        writer.print("上传的文件名称是:" + filename + "<br />");
                        // 截取出文件名
                        filename = filename.substring(filename.lastIndexOf("\\") + 1);
                        // 文件名需要唯一
                        filename = UUID.randomUUID().toString() + "_" + filename;
                        // 在服务器创建同名文件
                        String webPath = "/upload/";
                        //将服务器中文件夹路径与文件名组合成完整的服务器端路径
                        String filepath = getServletContext()
                                .getRealPath(webPath + filename);
                        // 创建文件
                        File file = new File(filepath);
                        file.getParentFile().mkdirs();
                        file.createNewFile();
                        // 获得上传文件流
                        InputStream in = fileitem.getInputStream();
                        // 使用FileOutputStream打开服务器端的上传文件
                        FileOutputStream out = new FileOutputStream(file);
                        // 流的拷贝
                        byte[] buffer = new byte[1024];//每次读取1个字节
                        int len;
                        //开始读取上传文件的字节,并将其输出到服务端的上传文件输出流中
                        while ((len = in.read(buffer)) > 0)
                            out.write(buffer, 0, len);
                        // 关闭流
                        in.close();
                        out.close();
                        // 删除临时文件
                        fileitem.delete();
                        writer.print("上传文件成功!<br />");
                    }
                }
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
    public void doPost(HttpServletRequest request,
                       HttpServletResponse response)throws ServletException, IOException {
        doGet(request, response);
    }
}

(2) web/06FileUpForm.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"%>
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>文件上传</title>
</head>
<body>
<form action="06uploadServlet" method="post"
      enctype="multipart/form-data">
    <table width="600px">
        <tr>
            <td>上传者</td>
            <td><input type="text" name="name" /></td>
        </tr>
        <tr>
            <td>上传文件</td>
            <td><input type="file" name="myfile"></td>
        </tr>
        <tr>
            <td colspan="2"><input type="submit" value="上传" /></td>
        </tr>
    </table>
</form>
</body>
</html>

运行结果如下:

图13 commons-fileupload和commons-io实现文件上传

4.4 使用API实现文件上传

核心:servlet3.0的Part接口

@MultipartConfig注解有四个属性

(1) cn/xj/servlet/UploadServlet2.java

package cn.xj.servlet;

import javax.servlet.ServletException;
import javax.servlet.annotation.MultipartConfig;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.Part;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Collection;

@WebServlet("/uploadServlet2")
// 使用注解@MultipartConfig将一个Servlet标识为支持文件上传
@MultipartConfig
public class UploadServlet2 extends HttpServlet {
    private static final long serialVersionUID = 1L;

    public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        request.setCharacterEncoding("utf-8");
        response.setCharacterEncoding("utf-8");
        response.setContentType("text/html;charset=utf-8");

        // 存储路径
        String savePath = request.getServletContext().getRealPath("/upload");
        // 获取上传的文件集合
        Collection<Part> parts = request.getParts();
        String desc = request.getParameter("desc");
        PrintWriter out = response.getWriter();
        out.println("上传者="+desc+"<br>");
        for (Part part : parts) {
            // 获取文件名
            String fileName = part.getSubmittedFileName();
            // 把文件写到指定路径
            if (fileName != null) {
                part.write(savePath + File.separator + fileName);
                out.println("文件名称="+fileName+"<br>");
            }
        }
        out.println("上传成功<br>");
        out.flush();
        out.close();
    }

    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        doGet(request, response);
    }
}

(2) web/07FileUpForm.html

<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8">
  <title>Servlet3.1实现文件上传</title>
</head>
<body>
  <h2>Servlet3.1 API实现文件上传</h2>
  <hr>
  <fieldset>
    <legend> 上传多个文件 </legend>
    <form action="uploadServlet2" method="post" enctype="multipart/form-data">
      上传者: <input type="text" name="desc"> <br>
      上传文件: <input type="file" name="file" multiple> <br>
      <input type="submit" value="上传多个文件">
    </form>
  </fieldset>
</body>
</html>

运行结果如下:

图14 servlet3.0的Part接口实现多文件上传

4.5 文件下载原理

文件下载的要求

实现文件下载功能比较简单,文件下载一般不需要使用第三方组件实现,而是直接使用Servlet类和输入/输出流实现。与访问服务器文件不同的是,要实现文件的下载,不仅需要指定文件的路径,还需要在HTTP协议中设置两个响应消息头,具体如下:

// 设定接收程序处理数据的方式
Content-Disposition: attachment;
// 设定实体内容的MIME类型
filename = Content-Type:application/x-msdownload

文件下载的过程

浏览器通常会直接处理响应的实体内容,需要在HTTP响应消息中设置两个响应消息头字段,用来指定接收程序处理数据内容的方式为下载。当单击“下载”超链接时,系统将请求提交到对应的Servlet。在Servlet中,首先获取下载文件的地址,并根据文件下载地址创建文件字节输入流,然后通过输入流读取要下载的文件内容,最后将读取的内容通过输出流写到目标文件中。

4.6 动手实践:实现文件下载

cn/xj/servlet/DownloadServlet.java

package cn.xj.servlet;
import java.io.*;
import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.*;
@WebServlet("/08downloadServlet")
public class DownloadServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;
    public void doGet(HttpServletRequest request, HttpServletResponse
            response) throws ServletException, IOException {
        //设置ContentType字段值
        response.setContentType("text/html;charset=utf-8");
        //获取所要下载的文件名称
        String filename = request.getParameter("filename");
        //下载文件所在目录
        String folder = "/download/";
        // 通知浏览器以下载的方式打开
        response.addHeader("Content-Type", "application/octet-stream");
        response.addHeader("Content-Disposition",
                "attachment;filename="+filename);
        folder=folder+filename;
        // 通过文件流读取文件
        InputStream in = getServletContext().getResourceAsStream(folder);
        // 获取response对象的输出流
        OutputStream out = response.getOutputStream();
        byte[] buffer = new byte[1024];
        int len;
        //循环取出流中的数据
        while ((len = in.read(buffer)) != -1) {
            out.write(buffer, 0, len);
        }
    }
    public void doPost(HttpServletRequest request, HttpServletResponse
            response) throws ServletException, IOException {
        doGet(request, response);
    }
}

web/08download.jsp

<%@ page contentType="text/html; charset=UTF-8"%>
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>文件下载</title>
</head>
<body>
<a href= "08downloadServlet?filename=1.png">
    文件下载</a>
</body>
</html>

运行结果如下:

图15 文件下载

5. 过滤器、监听器应用

5.1 首页(09/index_xj.jsp)访问次数统计

cn/xj/filter/CountFilter_xj.java

package cn.xj.filter;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.annotation.WebInitParam;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

@WebFilter(
        urlPatterns = { "/09/index_xj.jsp" },
        initParams = {
                @WebInitParam(name = "09count", value = "5000")
        })
public class CountFilter_xj implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        String param = filterConfig.getInitParameter("09count");
        filterConfig.getServletContext().setAttribute("count",param);
    }

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

        // 将ServletRequest转换成HttpServletRequest
        HttpServletRequest req = (HttpServletRequest) servletRequest;
        // 获取ServletContext
        ServletContext context = req.getServletContext();
        int count = Integer.parseInt((String) context.getAttribute("count"));        // 访问数量自增
        count ++;
        // 将来访数量值放入到ServletContext中
        context.setAttribute("count", Integer.toString(count));
        // 向下传递过滤器
        filterChain.doFilter(req, servletResponse);
    }
}

web/09/index_xj.jsp

<%@ page contentType="text/html;charset=UTF-8"%>
<html>
<head>
  <meta charset="UTF-8">
  <title>首页访问次数统计</title>
</head>
<body>
  <h2>首页访问次数统计</h2>
  <hr>
  <h2>欢迎光临,您是本站的第【 <%=application.getAttribute("count") %> 】次访访问!</h2>
</body>
</html>

运行结果如下:

图16 首页访问次数统计

5.2 图片资源防盗过滤器(保护/images/下资源)

web/images 4张图片

web/images/126.jpg

web/images/127.jpg

web/images/128.jpg

web/images/129.jpg

web/error.gif

web/images/test.txt

春风不度玉门关

cn/xj/filter/ImageRedirectFilter.java

package cn.xj.filter;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebFilter("/images/*")
public class ImageRedirectFilter implements Filter {

    public void doFilter(ServletRequest req, ServletResponse res,
                         FilterChain chain) throws IOException, ServletException {

        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;

        // 禁止缓存
        response.setHeader("Cache-Control", "no-store");
        response.setHeader("Pragrma", "no-cache");
        response.setDateHeader("Expires", 0);

        // 链接来源地址
        String referer = request.getHeader("referer");
        if (referer == null || !referer.contains(request.getServerName())) {
            // 如果 链接地址来自其他网站,则返回错误图片
            request.getRequestDispatcher("/error.gif").forward(request, response);
        } else {
            //图片资源请求通过
            chain.doFilter(request, response);
        }
    }
}

web/10index_xj.jsp

<%@ page contentType="text/html;charset=UTF-8"%>
<html>
<head>
    <meta charset="UTF-8">
    <title>图片防盗链</title>
</head>
<body>
    <h2>图片防盗链</h2>
    <hr>
    <img alt="可爱的孩子1" src="images/126.jpg"><br>
    <img alt="可爱的孩子2" src="images/127.jpg"><br>
    <img alt="可爱的孩子3" src="images/128.jpg"><br>
    <img alt="可爱的孩子4" src="images/129.jpg"><br>
    <a href="images/test.txt">文本连接</a><br>
</body>
</html>

运行结果如下:

图17 资源防盗链

5.3 Login过滤器(只有登录的用户才能访问/ch09_xj/11/目录及其子目录的资源)

web/11/test.html, web/11/sub/test.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>劝君更尽一杯酒,西出阳关无故人。</title>
</head>
<body>
劝君更尽一杯酒,西出阳关无故人。
</body>
</html>

web/11/test.txt, web/11/sub/test.txt

莫愁前路无知己,天下谁人不识君。

web/11/test.jpg, web/11/sub/test.jpg

cn/xj/servlet/LoginServlet11.java

package cn.xj.servlet;

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

@WebServlet("/11/loginServlet")
public class LoginServlet11 extends HttpServlet {
    private static final long serialVersionUID = 1L;

    public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        this.doPost(request, response);
    }

    public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        request.setCharacterEncoding("utf-8");
        // 获取用户名和密码
        String name = request.getParameter("username");
        String pass = request.getParameter("password");

        // 验证用户名和密码是否正确
        if (checkLogin(name, pass)) {
            // 判断是自动登录处理
            request.getSession().setAttribute("user11", name);
            request.getSession().removeAttribute("msg");
            // 成功登陆后的操作
            response.sendRedirect("index.jsp");
        } else {
            request.getSession().removeAttribute("user11");
            request.getSession().setAttribute("msg", "用户名或者密码错误");
            response.sendRedirect("login.jsp");
        }

    }

    protected boolean checkLogin(String name, String pass) {
        return "许杰".equals(name) && "123456".equals(pass);
    }
}

cn/xj/filter/LoginFilter.java

package cn.xj.filter;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;

@WebFilter(urlPatterns = { "/11/*" }, dispatcherTypes = {
        DispatcherType.REQUEST, DispatcherType.FORWARD, DispatcherType.INCLUDE,
        DispatcherType.ERROR })
public class LoginFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain) throws IOException, ServletException {
        // 把ServletRequest和ServletResponse转换成真正的类型
        HttpServletRequest req = (HttpServletRequest) request;
        HttpSession session = req.getSession();

        // 由于web.xml中设置Filter过滤全部请求,可以排除不需要过滤的url
        String requestURI = req.getRequestURI();
        if (requestURI.endsWith("/11/loginServlet")) {
            chain.doFilter(request, response);
            return;
        }
        if (requestURI.endsWith("/11/login.jsp")) {
            chain.doFilter(request, response);
            return;
        }

        // 判断用户是否登录,进行页面的处理
        if (null == session.getAttribute("user11")) {
            // 未登录用户,重定向到登录页面
            ((HttpServletResponse) response).sendRedirect(req.getContextPath()
                    + "/11/login.jsp");
            return;
        } else {
            // 已登录用户,允许访问
            chain.doFilter(request, response);
        }
    }
}

web/11/login.jsp

<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>系统登录</title>
</head>
<body>
  ${sessionScope.msg}<br>
  <form method="post" action="loginServlet">
    <label for="user">用户名</label><input type="text" name="username" id="user" required><br>
    <label for="pass">密码</label><input type="password" name="password" id="pass" required><br>
    <input type="submit" name="Submit" value="登录">
  </form>
  <hr>
  <a href="index.jsp">登录前访问index.jsp</a><br>
  <a href="test.html">登录前访问test.html</a><br>
  <a href="test.txt">登录前访问test.txt</a><br>
  <a href="test.jpg">登录前访问test.jpg</a><br>
  <a href="abcd.jpg">登录前访问不存在的abcd.jpg</a><br>
  <a href="sub/test.html">登录前访问sub/test.html</a><br>
  <a href="sub/test.txt">登录前访问sub/test.txt</a><br>
  <a href="sub/test.jpg">登录前访问sub/test.jpg</a><br>
  <a href="sub/abcd.jpg">登录前访问不存在的sub/abcd.jpg</a><br>
</body>
</html>

web/11/logOut.jsp

<%@ page contentType="text/html; charset=UTF-8"  pageEncoding="UTF-8"%>
<%
    session.invalidate();
%>
<jsp:forward page="login.jsp" />

web/11/index.jsp

<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html >
<html>
<head>
  <meta charset="UTF-8">
  <title>XXXX管理系统</title>
</head>
<body>
<h1>XXXX管理系统</h1>
<hr>
用户名:${sessionScope.user}|<a href="logOut.jsp">用户注销</a>
<hr>
<a href="index.jsp">登录后访问index.jsp</a><br>
<a href="test.html">登录后访问test.html</a><br>
<a href="test.txt">登录后访问test.txt</a><br>
<a href="test.jpg">登录后访问test.jpg</a><br>
<a href="abcd.jpg">登录后访问不存在的abcd.jpg</a><br>
<a href="sub/test.html">登录后访问sub/test.html</a><br>
<a href="sub/test.txt">登录后访问sub/test.txt</a><br>
<a href="sub/test.jpg">登录后sub/test.jpg</a><br>
<a href="sub/abcd.jpg">登录后访问不存在的sub/abcd.jpg</a><br>
</body>
</html>

运行结果如下:

图18 Login过滤器(只有登录的用户才能访问/ch09_xj/11/目录及其子目录的资源)

5.4 敏感文字过滤(过滤:糟糕,混蛋,他妈的,他奶奶的)

cn/xj/servlet/MessageServlet.java

package cn.xj.servlet;

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

@WebServlet("/12/MessageServlet")
public class MessageServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;

    public void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        // 获取标题, 内容
        String title = request.getParameter("title");
        String content = request.getParameter("content");

        // 将标题、内容放置到request中
        request.setAttribute("title", title);
        request.setAttribute("content", content);

        // 转发到result.jsp页面
        request.getRequestDispatcher("result.jsp").forward(request, response);
    }
}

cn/xj/filter/WordFilter.java

package cn.xj.filter;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.annotation.WebInitParam;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.IOException;

@WebFilter(urlPatterns = { "/12/*" }, initParams = { @WebInitParam(name = "encoding", value = "utf-8") })
public class WordFilter implements Filter {

    private String[] words; // 非法字符数组
    private String encoding; // 字符编码

    @Override
    public void init(FilterConfig fConfig) throws ServletException {
        // 获取字符编码
        encoding = fConfig.getInitParameter("encoding");
        // 初始化非法字符数组
        words = new String[] { "糟糕", "混蛋", "他妈的", "他奶奶的" };
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        // 判断字符编码是否有效
        if (encoding != null) {
            // 设置request字符编码
            request.setCharacterEncoding(encoding);
            // 将request转换为重写后的Request对象
            request = new Request((HttpServletRequest) request);
            // 设置response字符编码
            response.setContentType("text/html; charset=" + encoding);
        }
        chain.doFilter(request, response);
    }

    /**
     * 内部类重写HttpServletRequestWrapper
     */
    class Request extends HttpServletRequestWrapper {
        // 构造方法
        public Request(HttpServletRequest request) {
            super(request);
        }

        // 重写getParameter()方法
        @Override
        public String getParameter(String name) {
            // 返回过滤后的参数值
            return filter(super.getRequest().getParameter(name));
        }

        // 重写getParameterValues()方法
        @Override
        public String[] getParameterValues(String name) {
            // 获取所有参数值
            String[] values = super.getRequest().getParameterValues(name);
            // 通过循环对所有参数值进行过滤
            for (int i = 0; i < values.length; i++) {
                values[i] = filter(values[i]);
            }
            // 返回过滤后的参数值
            return values;
        }
    }

    /**
     * 过滤非法字符
     * @param param   参数值
     * @return 过滤后的参数值
     */
    public String filter(String param) {
        try {
            // 判断非法字符是否被初始化
            if (words != null && words.length > 0) {
                // 循环替换非法字符
                for (String word : words) {
                    // 判断是否包含非法字符
                    if (param.contains(word)) {
                        // 将非法字符替换为"****"
                        param = param.replaceAll(word, "****");
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return param;
    }

}

web/12/index.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
         pageEncoding="UTF-8"%>
<!DOCTYPE HTML>
<html>
<head>
    <meta charset="utf-8">
    <title>意见反馈</title>
</head>
<body>
<form action="MessageServlet" method="post">
    <fieldset>
        <legend>意见反馈</legend>
        <label>标题:</label><input type="text" name="title" size="30"><br>
        <label>内容:</label><textarea rows="5" cols="40" name="content"></textarea><br>
        <input type="submit" value="提 交">
    </fieldset>
</form>
</body>

web/12/result.jsp

<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%
  String title = (String) request.getAttribute("title");
  String content = (String) request.getAttribute("content");
  if (title != null && !title.isEmpty()) {
    out.println("<h2'>标题:" + title + "</h2>");
  }
  if (content != null && !content.isEmpty()) {
    out.println("<p>内容:" + content + "</p>");
  }
%>

运行结果如下:

图19 敏感文字过滤

5.5 登录(使用监听查看在线用户)

cn.xj.entity.UserInfo.java

package cn.xj.entity;


public class UserInfo {
    private String 登录名;
    private String 会话Id;  //当前用户的session id
    private String ip;   //当前用户的ip地址
    private String 登录时间;

    @Override
    public String toString() {
        return "UserInfo{" +
                "登录名='" + 登录名 + '\'' +
                ", 会话Id='" + 会话Id + '\'' +
                ", ip='" + ip + '\'' +
                ", 登录时间='" + 登录时间 + '\'' +
                '}';
    }

    public String get登录名() {
        return 登录名;
    }

    public void set登录名(String 登录名) {
        this.登录名 = 登录名;
    }

    public String get会话Id() {
        return 会话Id;
    }

    public void set会话Id(String 会话Id) {
        this.会话Id = 会话Id;
    }

    public String getIp() {
        return ip;
    }

    public void setIp(String ip) {
        this.ip = ip;
    }

    public String get登录时间() {
        return 登录时间;
    }

    public void set登录时间(String 登录时间) {
        this.登录时间 = 登录时间;
    }
}

cn.xj.util.SessionUtil.java

package cn.xj.util;

import cn.xj.entity.UserInfo;
import java.util.ArrayList;

public class SessionUtil {
    //根据sessionId判断当前用户是否存在在集合中 如果存在 返回当前用户 否则返回null
    public static UserInfo getUserBySessionId(ArrayList<UserInfo> userList, String sessionId) {
        for (UserInfo user : userList) {
            if(sessionId.equals(user.get会话Id())){
                return user;
            }
        }
        return null;
    }
}

cn/xj/listener/MyHttpSessionListener.java

package cn.xj.listener;

import cn.xj.entity.UserInfo;
import cn.xj.util.SessionUtil;

import javax.servlet.annotation.WebListener;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
import java.util.ArrayList;

@WebListener
public class MyHttpSessionListener implements HttpSessionListener {
    @Override
    public void sessionDestroyed(HttpSessionEvent se) {
        //sessionDestroyed 用户数-1
        int userCounts = (int)se.getSession().getServletContext().getAttribute("userCounts");
        if (userCounts > 0 ) userCounts--;
        //重新在servletContext中保存userCounts
        se.getSession().getServletContext().setAttribute("userCounts", userCounts);
        @SuppressWarnings("unchecked")
        ArrayList<UserInfo> userList=(ArrayList<UserInfo>) se.getSession().getServletContext().getAttribute("userList");
        String sessionId=se.getSession().getId();
        //如果当前用户在userList中 在session销毁时 将当前用户移出userList
        if(SessionUtil.getUserBySessionId(userList, sessionId)!=null){
            userList.remove(SessionUtil.getUserBySessionId(userList, sessionId));
        }
        //将userList集合 重新保存到servletContext
        se.getSession().getServletContext().setAttribute("userList", userList);
    }
}

cncn/xj/listener/MyServletRequestListener.java

package cn.xj.listener;

import cn.xj.entity.UserInfo;
import cn.xj.util.SessionUtil;

import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.annotation.WebListener;
import javax.servlet.http.HttpServletRequest;
import java.io.UnsupportedEncodingException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;


/**
 * ServletRequestListener 监听器
 *
 */

@WebListener
public class MyServletRequestListener implements ServletRequestListener {

    //用户集合
    private ArrayList<UserInfo> userList;


    @SuppressWarnings("unchecked")
    @Override
    public void requestInitialized(ServletRequestEvent arg0) {
        //从servletContext中获的userList
        userList = (ArrayList<UserInfo>) arg0.getServletContext().getAttribute("userList");
        //如果servletContext中没有userList对象 初始化userList
        if (userList == null) {
            userList = new ArrayList<UserInfo>();
        }

        HttpServletRequest request = (HttpServletRequest) arg0.getServletRequest();
        try {
            request.setCharacterEncoding("utf-8");
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
        if(request.getRequestURI().endsWith("/13/adxjser.jsp")) {
            // 用户数+1
            int userCounts;
            Object userCounts1 = arg0.getServletContext().getAttribute("userCounts");
            if (userCounts1 == null)
                userCounts=0;
            else
                userCounts=(int) userCounts1;
            userCounts++;
            //重新在servletContext中保存userCounts
            arg0.getServletContext().setAttribute("userCounts", userCounts);

            //sessionId
            String sessionId = request.getSession().getId();
            String username = request.getParameter("登录名");
            request.getSession().setMaxInactiveInterval(10);
            //如果当前sessionId不存在集合中 创建当前user对象
            if (SessionUtil.getUserBySessionId(userList, sessionId) == null) {
                UserInfo user = new UserInfo();
                user.set会话Id(sessionId);
                user.set登录名(username);
                user.setIp(request.getRemoteAddr());
                SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 E hh:mm:ss");
                user.set登录时间(sdf.format(new Date()));
                userList.add(user);
            }

            //将userList集合 保存到ServletContext
            arg0.getServletContext().setAttribute("userList", userList);
        }
    }
}

web/13/adxjser.jsp

<%@ page contentType="text/html; charset=UTF-8" %>
<%
  response.sendRedirect("showUser.jsp");
%>

web/13/showUser.jsp

<%@ page contentType="text/html; charset=UTF-8" errorPage=""%>
<%@ page import="java.util.*"%>
<%@ page import="cn.xj.entity.UserInfo" %>
<html>
<head>
  <meta charset="UTF-8">
  <title>使用监听查看在线用户</title>
</head>
<body>
  <%
    ArrayList<UserInfo> userList = (ArrayList<UserInfo>) request.getServletContext().getAttribute("userList");
  %>
  <h2>查看在线用户(20秒不和服务器交互、服务器端自动断开)</h2><hr>
  在线人数:${userCounts} <br>
  <textarea rows="8" cols="160">
<%
  if(userList!=null && userList.size()>0){
    for(UserInfo userInfo : userList ){
      out.println(userInfo.toString());
    }
  }
%>
  </textarea> <br>
  <a href="loginOut.jsp">安全退出(注销、登出)</a>
</body>
</html>

web/13/loginOut.jsp

<%@ page contentType="text/html; charset=UTF-8"  %>
<%
    session.invalidate();
    out.println("<script>parent.location.href='index.jsp';</script>");
%>

web/13/index.jsp

<%@ page contentType="text/html; charset=UTF-8" errorPage=""%>
<html>
<head>
  <meta charset="UTF-8">
  <title>使用监听查看在线用户</title>
</head>
<body>
<form name="form" method="post" action="adxjser.jsp">
  <input type="text" name="登录名" required><br>
  <input   type="submit" name="Submit" value="登录">
</form>
</body>
</html>

运行结果如下:

图20 使用监听查看在线用户
图21 ch09源代码文件结构




Servlet3.0、4.0的新特性

概述3.0新特性:

以前的Servlet的流程:

  1. Servlet接收到请求之后,需要对请求携带的数据进行一些处理
  2. 调用业务接口的某些方法,已完成业务处理
  3. 根据处理的结果提交响应,Servlet线程结束

其中第二步的业务处理通常是最耗时的,这主要体现数据库操作,以及其它的跨网络调用等、在此过程中、Servlet线程一直处于阻塞状态,直到业务方法执行完毕。在处理业务过程中、Servlet资源一直被占用而得不到释放,对于并发较大的应用,这有可能造成性能瓶颈,对此,在以前通常采用私有解决方案,来提前结束Servlet线程,并及时释放资源。

现在的Servlet的流程:

  1. Servlet接收到请求之后,需要对请求携带的数据进行一些处理
  2. Servlet线程将请求转交给一个异步线程来执行业务处理,线程本身返回至容器。此时Servlet还没有生成响应数据,异步线程处理完业务以后,可以直接生成响应数据(异步线程拥有ServletRequest和ServletResponse对象的引用),或者将请求继续转发给其它的Servlet

如此一来,Servlet线程不再是一直处于阻塞状态以等待业务逻辑的处理,而是启动异步线程之后可以立即返回。

概述4.0新特性:

Servlet4.0的优点:

在执行所有这些操作的同时,服务器仍在处理原始网页请求。客户端收到响应时,它需要的资源已经位于缓存中。Servlet 4.0 通过 PushBuilder 接口公开服务器推送、也可以推送静态资源等。


Servlet3.0新特性:异步处理,太好用了!!!

https://itsoku.blog.csdn.net/article/details/115475169


返回