Servlet的体系

Web项目是B/S结构,浏览器/服务器模式的。浏览器发起请求,服务器作出响应,请求的发起和响应使用HTTP协议进行通讯,所谓协议也就是一种固定格式。而Socket是应用层与传输层的一层编程接口,屏蔽了传输层的细节,所以Web项目就是通过Socket发送HTTP请求和响应的过程,只不过请求是浏览器发出来的,响应是服务器发出来的。针对于JavaWeb项目,动态响应则是Servlet容器通过调用Servlet进行响应,程序员所要做的也就只是编写符合Servlet规范的Servlet。

一个基本的请求--响应调用过程:

什么是简介

Servlet(Server Applet),全称Java Servlet。是用Java编写的服务器端程序。其主要功能在于交互式地浏览和修改数据,生成动态Web内容。狭义的Servlet是指Java语言实现的一个接口,广义的Servlet是指任何实现了这个Servlet接口的类,一般情况下,人们将Servlet理解为后者。

Servlet运行于支持Java的应用服务器中。从实现上讲,Servlet可以响应任何类型的请求,但绝大多数情况下Servlet只用来扩展基于HTTP协议的Web服务器。

Servlet核心的类和接口

Servlet API主要由两个包组成: javax.servlet和javax.servlet.http。

  • 在javax.servlet包中定义了Servlet接口及相关的通用接口和类;
  • 在javax.servlet.http包中主要定义了与HTTP协议相关的HttpServlet类,HttpServletRequest接口和HttpServletResponse接口;

Servlet接口及实现类

Servlet是基于Java 技术的web组件,容器托管的,用于生成动态内容。

Servlet是顶级接口,表示服务端的运行的程序。

GenericServlet抽象类为Servlet接口提供了通用实现,想要实现Servlet只需要继承GenericServlet即可,它与任何网络应用层协议无关。

HttpServlet是针对于HTTP请求的Servlet实现。

为什么要绕这么多层,是因为Servlet不针对任何具体的协议,是协议独立的。虽然web应用人员基本上都是在使用HttpServlet,但是并不代表它就只是http协议。

Servlet接口定义:

public void init(ServletConfig config)
public void service(ServletRequest req, ServletResponse res)
public void destroy();

这三个方法都是生命周期方法,由容器负责调用。

创建Servlet的三种方式:

  • 定义一个类实现javax.servlet.Servlet接口
  • 定义一个类继承javax.servet.GenericServlet类
  • 定义一个类继承javax.servlet.http.HttpServlet类 web应用我们自然基本上都是继承HttpServlet

ServletRequest接口及HttpServletRequest实现类

ServletRequest是javax.servlet.Servlet#service方法的参数,它表示请求对象,它封装了所有与请求相关的数据,它是由服务器创建的。

ServletRequest常用的方法:

  • getContentLength():返回请求正文的长度,如果请求正文的长度未知,则返回-1;
  • getContentType():获得请求正文的MIME类型,如果请求正文的类型不存在,则返回null;
  • getInputStream():返回用于读取请求正文的输入流;
  • getLocalAddr():返回服务端的IP地址;
  • getLocalName():返回服务端的主机名;
  • getLocalPort():返回服务端的端口号;
  • getParameters():根据给定的请求参数名,返回来自客户请求中的匹配的请求参数值;
  • getProtocal():返回客户端与服务器端通信所用的协议名称及版本号;
  • getReader():返回用于读取字符串形式的请求正文的BufferReader对象;
  • getRemoteAddr():返回客户端的IP地址;
  • getRemoteHost():返回客户端的主机名;
  • getRemotePort():返回客户端的端口号

HttpServletRequest接口提供了用于读取HTTP请求中的相关信息的方法:

  • getContextPath():返回客户端请求方法的Web应用的URL入口,例如,如果客户端访问的URL为http://localhost:8080/myApp/index,那么该方法返回“/myApp”;
  • getCookies():返回HTTP请求中的所有Cookie;
  • getHeader(String name):返回HTTP请求头部的特定项;
  • getHeaderName():返回一个Enumeration对象,它包含了HTTP请求头部的所有项目名;
  • getMethod():返回HTTP请求方式;
  • getRequestURL():返回HTTP请求的头部的第一行中的URL;
  • getQueryString():返回HTTP请求中的查询字符串,即URL中的“?”后面的内容;

ServletResponse接口及HttpServletResponse实现类

ServletResponse常用的方法:

  • setCharacterEncoding():设置相应正文的字符编码。响应正文的默认字符编码为ISO-8859-1;
  • setContentLength():设置响应正文的长度;
  • setContentType():设置响应正文的MIME类型;
  • setBufferSize():设置用于存放响应正文数据的缓冲区的大小
  • reset():清空缓冲区内的正文数据,并且清空响应状态代码及响应头
  • resetBuffer():仅仅清空缓冲区的正文数据,不清空响应状态代码及响应头;
  • flushBuffer():强制性地把缓冲区内的响应正文数据发送到客户端;
  • isCommitted():返回一个boolean类型的值,如果为true,表示缓冲区内的数据已经提交给客户,即数据已经发送到客户端;
  • getCharacterEncoding():获得响应正文的字符编码
  • getContentType():获得响应正文的MIME类型
  • getBufferSize():获得用于存放响应正文数据的缓冲区的大小;
  • getOutputStream():返回一个ServletOutputStream对象,Servlet用它来输出二进制的正文数据;
  • getWriter():返回一个PrinterWriter对象,Servlet用它来输出字符串形式的正文数据;

ServletResponse中响应正文的默认MIME类型是text/plain,即纯文本类型,而HttpServletResponse中响应正文的默认MIME类型为text/html,即HTML文档类型。

为了提高输出数据的效率,ServletOutputStream和PrintWriter首先把数据写到缓冲区内。当缓冲区内的数据被提交给客户后,ServletResponse的isComitted方法返回true。

在以下几种情况下,缓冲区内的数据会被提交给客户,即数据被发送到客户端:

  • 当缓冲区内的数据已满时,ServletOutPutStream或PrintWriter会自动把缓冲区内的数据发送给客户端,并且清空缓冲区;
  • Servlet调用ServletResponse对象的flushBuffer方法;
  • Servlet调用ServletOutputStream或PrintWriter对象的flush方法或close方法;

如果要设置响应正文的MIME类型和字符编码,必须先调用ServletResponse对象的setContentType()和setCharacterEncoding()方法,然后再调用ServletResponse的getOutputStream()或getWriter()方法。

HttpServletResponse接口提供了与HTTP协议相关的一些方法,Servlet可通过这些方法来设置HTTP响应头或向客户端写Cookie。

  • addHeader():向HTTP响应头中加入一项内容
  • sendError():向客户端发送一个代表特定错误的HTTP响应状态代码
  • setHeader():设置HTTP响应头中的一项内容,如果在响应头中已经存在这项内容,则原来的设置被覆盖
  • setStatus():设置HTTP响应的状态代码
  • addCookie():向HTTP响应中加入一个Cookie

在HttpServletResponse接口中定义了一些代表HTTP响应状态代码的静态常量:

Servlet容器是调度器,Servlet 是处理器,ServletRequest是请求,ServletResponse是响应。

ServletConfig

ServletConfig是Servlet的配置信息。servlet容器用于在Servlet初始化时传递信息的Servlet配置对象,一个Servlet对应一个ServletConfig。

当servlet配置了初始化参数后,web容器在创建servlet实例对象时,会自动将这些初始化参数封装到ServletConfig对象中。

ServletConfig是Servlet的init方法的参数,init方法由Servlet容器调用  Servlet容器将ServletConfig参数传递过来:

public void init(ServletConfig config) throws ServletException;

常用方法:

  • getInitParameter(String name):返回匹配的初始化参数值
  • getInitParameterNames():返回一个Enumeration对象,里面包含了所有的初始化参数名
  • getServletContext():返回一个ServletContext对象
  • getServletName():返回Servlet的名字,即web.xml文件中相应<servlet>元素的<servlet-name>子元素的值;如果没有为servlet配置<servlet-name>子元素,则返回Servlet类的名字

GenericServlet, HttpServlet实现了ServletConfig接口。

HttpServlet类继承了GenericServlet类,而GenericServlet类实现了ServletConfig接口,因此HttpServlet或GenericServlet类及子类中都可以直接调用ServletConfig接口中的方法。

ServletContext

ServletContext定义了servlet用来与servlet容器通信的一组方法,例如,获取文件的MIME类型、分派请求或写入日志文件。

ServletContext对象包含在ServletConfig对象中,ServletConfig是Web服务器在servlet被初始化时,提供给servlet

所以可以使用:Servlet.getServletConfig() 或者 ServletConfig.getServletContext() 获取Context。

一个应用一个JVM只有一个Context也就是所有应用共享的。

ServletContext接口提供的方法可以分为以下几种类型:

1、用于在web应用范围内存取共享数据的方法

setAttribute(String name, Object object) —— 把一个Java对象与一个属性名绑定,并存入到ServletContext中;
getAttribute() —— 返回指定数姓名的属性值
getAttributeNames() —— 返回一个Enumeration对象,包含所有存放在ServletContext中的属性名
removeAttributes() —— 从ServletContext中删除匹配的属性

2、访问当前Web应用的资源

getContextPath() —— 返回当前Web应用的URL入口
getInitParameter() —— 返回Web应用范围内的匹配的初始化参数值。在web.xml中,直接在<web-app>根元素下定义的<context-param>元素表示应用范围内的初始化参数
getServletContextName() —— 返回Web应用的名字,即web.xml文件中<display-name>元素的值
getRequestDispatcher() —— 返回一个用于向其他WEB组件转发请求的RequestDispatcher对象

3、访问服务器端的文件系统资源

getRealPath() —— 根据参数指定的虚拟路径,返回文件系统中的一个真实的路径
getResources() —— 返回一个映射到参数指定的路径的URL
getResourceAsStream() —— 返回一个用于读取参数指定的文件的输入流
getMimeType() —— 返回参数指定的文件MIME类型

4、输出日志

log(String msg) —— 向Servlet的日志文件中写日志
log(String message, Throwable throwable) —— 向Servlet的日志文件中写入错误日志,以及异常的堆栈信息

创建Web项目及Servlet示例

1、创建一个Web项目

说明:如果使用Maven创建,可以选择org.apache.maven.archetypes:maven-archetype-webapp模板。

web project 名称
    |--src
        | --main
            | --java
            | --resources
            | --webapp
                | --WEB-INF
                    | --web.xml
        | --test
            | --java
            | --resources
    | --pom.xml

WEB-INF是Java的WEB应用的安全目录,客户端无法访问,只能通过服务端访问,从而实现了代码的安全。

在WEB-INF中主要是系统运行的配置信息和环境,主要有classes、config、lib文件夹和web.xml。

pom.xml引入Servlet依赖:

<packaging>war</packaging>
<dependencies>
    <!-- servlet -->
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>javax.servlet-api</artifactId>
      <version>4.0.1</version>
      <scope>provided</scope>
    </dependency>
</dependencies>

2、继承HttpServlet,创建Servlet

/**
 * @author linhongwei
 * @version 1.0.0
 * @ClassName ServletDemo.java
 * @Description HttpServlet的方法可以选择性重写
 * @createTime 2021年08月18日 08:35:00
 */
public class ServletDemo extends HttpServlet {

    @Override
    public void init() throws ServletException {
        System.out.println(">>>>>>>>>>>>>>>init");
    }

    @Override
    public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
        System.out.println(">>>>>>>>>>>>>>>service");
    }

    @Override
    public void destroy() {
        System.out.println(">>>>>>>>>>>>>>>destroy");
    }
}

3、web.xml配置

<?xml version="1.0" encoding="UTF-8"?>
<!--
    web.xml不同版本的头
    2.3/2.4/2.5/3.0
-->
<web-app
        version="3.0"
        xmlns="http://java.sun.com/xml/ns/javaee"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">

    <!-- 配置 Servlet-->
    <servlet>
        <servlet-name>ServletDemo</servlet-name>
        <servlet-class>com.harvey.web.ServletDemo</servlet-class>
        <load-on-startup>0</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>ServletDemo</servlet-name>
        <url-pattern>/first</url-pattern>
    </servlet-mapping>
</web-app>

4、部署

使用idea工具将web项目的war包部署到tomcat中(或者放到webapps目录下),直接启动tomcat即可,tomcat的默认端口是8080,我这里设置是8888。

5、访问

访问的时候注意项目是否有配置应用上下文,如果有的话,访问路径需要添加上。

http://localhost:8888/first 或 http://localhost:8888/应用上下文/first

Servlet生命周期

Servlet的运行原理:

Servlet生命周期定义了一个Servlet如何被加载、初始化,以及它怎样接收请求、响应请求,提供服务。在讨论Servlet生命周期之前,先让我们来看一下这几个方法:

1、 init()方法

在Servlet的生命周期中,仅执行一次init()方法,它是在服务器装入Servlet时执行的,可以配置服务器,以在启动服务器或客户机首次访问Servlet时装入Servlet。无论有多少客户机访问Servlet,都不会重复执行init();

2、service()方法

它是Servlet的核心,每当一个客户请求一个HttpServlet对象,该对象的Service()方法就要调用,而且传递给这个方法一个“请求”(ServletRequest)对象和一个“响应”(ServletResponse)对象作为参数。在HttpServlet中已存在Service()方法。默认的服务功能是调用与HTTP请求的方法相应的doXxx功能。

3、destroy()方法

仅执行一次,在服务器端停止且卸载Servlet时执行该方法,有点类似于C++的delete方法。一个Servlet在运行service()方法时可能会产生其他的线程,因此需要确认在调用destroy()方法时,这些线程已经终止或完成。

下面来谈谈Servlet的生命周期,Servlet的生命周期是由Servlet容器来控制的,它始于装入Web服务器的内存时,并在终止或重新装入Servlet时结束。这项操作一般是动态执行的。然而,Server通常会提供一个管理的选项,用于在Server启动时强制装载和初始化特定的Servlet。

在代码中,Servlet生命周期由接口javax.servlet.Servlet定义。所有的Java Servlet 必须直接或间接地实现javax.servlet.Servlet接口,这样才能在Servlet Engine上运行。javax.servlet.Servlet接口定义了一些方法,在Servlet 的生命周期中,这些方法会在特定时间按照一定的顺序被调用。

加载和实例化Servlet

我们来看一下tomcat是如何加载的:

  • 如果已配置自动装入选项,则在启动时自动载入。
  • 在服务器启动时,客户机首次向Servlet发出请求。
  • 重新装入Servlet时。

当启动Servlet容器时,容器首先查找一个配置文件web.xml,这个文件中记录了可以提供服务的Servlet。每个Servlet被指定一个Servlet名,也就是这个Servlet实际对应的Java的完整class文件名。Servlet容器会为每个自动装入选项的Servlet创建一个实例。所以,每个Servlet类必须有一个公共的无参数的构造器。

初始化

当Servlet被实例化后,Servlet容器将调用每个Servlet的init方法来实例化每个实例,执行完init方法之后,Servlet处于“已初始化”状态。所以说,一旦Servlet被实例化,那么必将调用init方法。通过Servlet在启动后不立即初始化,而是收到请求后进行。在web.xml文件中用<load-on-statup> ...... </load-on-statup>对Servlet进行预先初始化。

初始化失败后,执行init()方法抛出ServletException异常,Servlet对象将会被垃圾回收器回收,当客户端第一次访问服务器时加载Servlet实现类,创建对象并执行初始化方法。

请求处理

Servlet 被初始化以后,就处于能响应请求的就绪状态。每个对Servlet 的请求由一个ServletRequest 对象代表。Servlet 给客户端的响应由一个ServletResponse对象代表。对于到达客户机的请求,服务器创建特定于请求的一个“请求”对象和一个“响应”对象。调用service方法,这个方法可以调用其他方法来处理请求。

service方法会在服务器被访问时调用,Servlet对象的生命周期中service方法可能被多次调用,由于web-server启动后,服务器中公开的部分资源将处于网络中,当网络中的不同主机(客户端)并发访问服务器中的同一资源,服务器将开设多个线程处理不同的请求,多线程同时处理同一对象时,有可能出现数据并发访问的错误。

另外注意,多线程难免同时处理同一变量时(如:对同一文件进行写操作),且有读写操作时,必须考虑是否加上同步,同步添加时,不要添加范围过大,有可能使程序变为纯粹的单线程,大大削弱了系统性能;只需要做到多个线程安全的访问相同的对象就可以了。

卸载Servlet

当服务器不再需要Servlet实例或重新装入时,会调用destroy方法,使用这个方法,Servlet可以释放掉所有在init方法申请的资源。一个Servlet实例一旦终止,就不允许再次被调用,只能等待被卸载。

Servlet一旦终止,Servlet实例即可被垃圾回收,处于“卸载”状态,如果Servlet容器被关闭,Servlet也会被卸载,一个Servlet实例只能初始化一次,但可以创建多个相同的Servlet实例。如相同的Servlet可以在根据不同的配置参数连接不同的数据库时创建多个实例。

Get请求和Post请求

doGet()表示,当客户端是使用get方式请求该servlet时,那么就会触发执行doGet()方法中的代码。

doPost()表示,当客户端是使用post方式请求该servlet时,那么就会触发执行doPost()方法中的代码。

而service()方法,既可以接收并处理get请求,也可以接收并处理post请求。

如果这三个方法都被重写时,service()的优先级是高于doGet()和doPost()的,所以,即使客户端使用get或者post方式请求该servlet,但是服务器因为存在service()方法,即使存在doGet()和doPost(),也是会调用service()方法,而不会调用doGet()和doPost()方法的。

总结来说:如果servlet中有service()方法,那么会优先调用service()方法来处理请求

Servlet中主要http错误状态码的产生原因:

404(资源未找到)

  • 客户端发起的请求中,输入的url中的servlet拼写错误,在web.xml中的找不到对应的url-pattern(注意区分大小写)。
  • 客户端发起的请求中,输入的url中的项目名称错误(注意区分大小写)。

405(请求方式不支持)

  • 假设用户发起post请求,但是servlet中既没有重写service(),也没有重写doPost()方法,此时没有就会出现405错误。
  • 假设用户发起post请求,该servlet中没有重写doPost()方法,但是重写了service()方法,但是又在当前service()中调用super.service(),所以导致super.service()中调用doPost()失败。

500(服务器内部错误)

  • 在web.xml中servlet的全限定路径拼写错误,web.xml写的是aa.bb类,但是实际的java文件名称是a.b类,导致url-pattern符合规则时,去找对应的class文件时,没有找到。
  • servlet对象的处理逻辑出现错误,比如0作除数。 

HttpServletRequest和HttpServletResponse

HttpServletRequest

HttpServletRequest对象代表客户端的请求,当客户端通过HTTP协议访问服务器时,HTTP请求头中的所有信息都封装在这个对象中,通过这个对象提供的方法,可以获得客户端请求的所有信息。

常用方法

1、获得客户机信息

  • getRequestURL方法返回客户端发出请求时的完整URL。
  • getRequestURI方法返回请求行中的资源名部分。
  • getQueryString 方法返回请求行中的参数部分。
  • getPathInfo方法返回请求URL中的额外路径信息。额外路径信息是请求URL中的位于Servlet的路径之后和查询参数之前的内容,它以“/”开头。
  • getRemoteAddr方法返回发出请求的客户机的IP地址。
  • getRemoteHost方法返回发出请求的客户机的完整主机名。
  • getRemotePort方法返回客户机所使用的网络端口号。
  • getLocalAddr方法返回WEB服务器的IP地址。
  • getLocalName方法返回WEB服务器的主机名。

范例:通过request对象获取客户端请求信息

import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
 * 通过request对象获取客户端请求信息
 */
public class RequestDemo01 extends HttpServlet {

    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        /**
         * 1.获得客户机信息
         */
        String requestUrl = request.getRequestURL().toString();//得到请求的URL地址
        String requestUri = request.getRequestURI();//得到请求的资源
        String queryString = request.getQueryString();//得到请求的URL地址中附带的参数
        String remoteAddr = request.getRemoteAddr();//得到来访者的IP地址
        String remoteHost = request.getRemoteHost();
        int remotePort = request.getRemotePort();
        String remoteUser = request.getRemoteUser();
        String method = request.getMethod();//得到请求URL地址时使用的方法
        String pathInfo = request.getPathInfo();
        String localAddr = request.getLocalAddr();//获取WEB服务器的IP地址
        String localName = request.getLocalName();//获取WEB服务器的主机名
        response.setCharacterEncoding("UTF-8");//设置将字符以"UTF-8"编码输出到客户端浏览器
        //通过设置响应头控制浏览器以UTF-8的编码显示数据,如果不加这句话,那么浏览器显示的将是乱码
        response.setHeader("content-type", "text/html;charset=UTF-8");
        PrintWriter out = response.getWriter();
        out.write("获取到的客户机信息如下:");
        out.write("<hr/>");
        out.write("请求的URL地址:"+requestUrl);
        out.write("<br/>");
        out.write("请求的资源:"+requestUri);
        out.write("<br/>");
        out.write("请求的URL地址中附带的参数:"+queryString);
        out.write("<br/>");
        out.write("来访者的IP地址:"+remoteAddr);
        out.write("<br/>");
        out.write("来访者的主机名:"+remoteHost);
        out.write("<br/>");
        out.write("使用的端口号:"+remotePort);
        out.write("<br/>");
        out.write("remoteUser:"+remoteUser);
        out.write("<br/>");
        out.write("请求使用的方法:"+method);
        out.write("<br/>");
        out.write("pathInfo:"+pathInfo);
        out.write("<br/>");
        out.write("localAddr:"+localAddr);
        out.write("<br/>");
        out.write("localName:"+localName);
    }

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

}

运行界面如下:

2、获得客户机请求头

  • getHeader(string name)方法:String
  • getHeaders(String name)方法:Enumeration 
  • getHeaderNames()方法

范例:通过request对象获取客户端请求头信息

import java.io.IOException;
import java.io.PrintWriter;
import java.util.Enumeration;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
 * 获取客户端请求头信息
 * 客户端请求头:
 *
 */
public class RequestDemo02 extends HttpServlet {

    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        response.setCharacterEncoding("UTF-8");//设置将字符以"UTF-8"编码输出到客户端浏览器
        //通过设置响应头控制浏览器以UTF-8的编码显示数据
        response.setHeader("content-type", "text/html;charset=UTF-8");
        PrintWriter out = response.getWriter();
        Enumeration<String> reqHeadInfos = request.getHeaderNames();//获取所有的请求头
        out.write("获取到的客户端所有的请求头信息如下:");
        out.write("<hr/>");
        while (reqHeadInfos.hasMoreElements()) {
            String headName = (String) reqHeadInfos.nextElement();
            String headValue = request.getHeader(headName);//根据请求头的名字获取对应的请求头的值
            out.write(headName+":"+headValue);
            out.write("<br/>");
        }
        out.write("<br/>");
        out.write("获取到的客户端Accept-Encoding请求头的值:");
        out.write("<hr/>");
        String value = request.getHeader("Accept-Encoding");//获取Accept-Encoding请求头对应的值
        out.write(value);

        Enumeration<String> e = request.getHeaders("Accept-Encoding");
        while (e.hasMoreElements()) {
            String string = (String) e.nextElement();
            System.out.println(string);
        }
    }

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

}

运行结果如下:

3、获得客户机请求参数(客户端提交的数据)

  • getParameter(String)方法(常用)
  • getParameterValues(String name)方法(常用)
  • getParameterNames()方法(不常用)
  • getParameterMap()方法(编写框架时常用)

比如现在有如下的form表单:

<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
    <title>Html的Form表单元素</title>
</head>
<fieldset style="width:500px;">
    <legend>Html的Form表单元素</legend>
    <!--form表单的action属性规定当提交表单时,向何处发送表单数据,method属性指明表单的提交方式,分为get和post,默认为get-->
    <form action="${pageContext.request.contextPath}/servlet/RequestDemo03" method="post">
    <!--输入文本框,SIZE表示显示长度,maxlength表示最多输入长度-->
    编  号(文本框):
    <input type="text" name="userid" value="NO." size="2" maxlength="2"><br>
    <!--输入文本框,通过value指定其显示的默认值-->
    用户名(文本框):<input type="text" name="username" value="请输入用户名"><br>
    <!--密码框,其中所有输入的内容都以密文的形式显示-->
    密  码(密码框):
    <!-- 表示的是一个空格-->
    <input type="password" name="userpass" value="请输入密码"><br>
    <!--单选按钮,通过checked指定默认选中,名称必须一样,其中value为真正需要的内容-->
    性  别(单选框):
    <input type="radio" name="sex" value="男" checked>男
    <input type="radio" name="sex" value="女">女<br>
    <!--下拉列表框,通过<option>元素指定下拉的选项-->
    部  门(下拉框):
    <select name="dept">
        <option value="技术部">技术部</option>
        <option value="销售部" SELECTED>销售部</option>
        <option value="财务部">财务部</option>
    </select><br>
    <!--复选框,可以同时选择多个选项,名称必须一样,其中value为真正需要的内容-->
    兴  趣(复选框):
    <input type="checkbox" name="inst" value="唱歌">唱歌
    <input type="checkbox" name="inst" value="游泳">游泳
    <input type="checkbox" name="inst" value="跳舞">跳舞
    <input type="checkbox" name="inst" value="编程" checked>编程
    <input type="checkbox" name="inst" value="上网">上网
    <br>
    <!--大文本输入框,宽度为34列,高度为5行-->
    说  明(文本域):
    <textarea name="note" cols="34" rows="5">
     </textarea>
    <br>
    <!--隐藏域,在页面上无法看到,专门用来传递参数或者保存参数-->
    <input type="hidden" name="hiddenField" value="hiddenvalue"/>
    <!--提交表单按钮,当点击提交后,所有填写的表单内容都会被传输到服务器端-->
    <input type="submit" value="提交(提交按钮)">
    <!--重置表单按钮,当点击重置后,所有表单恢复原始显示内容-->
    <input type="reset" value="重置(重置按钮)">
</form>
<!--表单结束-->
</fieldset>
</body>
<!--完结标记-->
</html>
<!--完结标记-->

在服务器端使用getParameter方法和getParameterValues方法接收表单参数,代码如下:

import java.io.IOException;
import java.text.MessageFormat;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
 * 获取客户端通过Form表单提交上来的参数
 */
public class RequestDemo03 extends HttpServlet {

    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        //客户端是以UTF-8编码提交表单数据的,所以需要设置服务器端以UTF-8的编码进行接收,否则对于中文数据就会产生乱码
        request.setCharacterEncoding("UTF-8");
        /**
         * 编  号(文本框):
           <input type="text" name="userid" value="NO." size="2" maxlength="2">
         */
        String userid = request.getParameter("userid");//获取填写的编号,userid是文本框的名字,<input type="text" name="userid">
        /**
         * 用户名(文本框):<input type="text" name="username" value="请输入用户名">
         */
        String username = request.getParameter("username");//获取填写的用户名
        /**
         * 密  码(密码框):<input type="password" name="userpass" value="请输入密码">
         */
        String userpass = request.getParameter("userpass");//获取填写的密码
        String sex = request.getParameter("sex");//获取选中的性别
        String dept = request.getParameter("dept");//获取选中的部门
        //获取选中的兴趣,因为可以选中多个值,所以获取到的值是一个字符串数组,因此需要使用getParameterValues方法来获取
        String[] insts = request.getParameterValues("inst");
        String note = request.getParameter("note");//获取填写的说明信息
        String hiddenField = request.getParameter("hiddenField");//获取隐藏域的内容

        String instStr="";
        /**
         * 获取数组数据的技巧,可以避免insts数组为null时引发的空指针异常错误!
         */
        for (int i = 0; insts!=null && i < insts.length; i++) {
            if (i == insts.length-1) {
                instStr+=insts[i];
            }else {
                instStr+=insts[i]+",";
            }
        }

        String htmlStr = "<table>" +
                            "<tr><td>填写的编号:</td><td>{0}</td></tr>" +
                            "<tr><td>填写的用户名:</td><td>{1}</td></tr>" +
                            "<tr><td>填写的密码:</td><td>{2}</td></tr>" +
                            "<tr><td>选中的性别:</td><td>{3}</td></tr>" +
                            "<tr><td>选中的部门:</td><td>{4}</td></tr>" +
                            "<tr><td>选中的兴趣:</td><td>{5}</td></tr>" +
                            "<tr><td>填写的说明:</td><td>{6}</td></tr>" +
                            "<tr><td>隐藏域的内容:</td><td>{7}</td></tr>" +
                        "</table>";
        htmlStr = MessageFormat.format(htmlStr, userid,username,userpass,sex,dept,instStr,note,hiddenField);

        response.setCharacterEncoding("UTF-8");//设置服务器端以UTF-8编码输出数据到客户端
        response.setContentType("text/html;charset=UTF-8");//设置客户端浏览器以UTF-8编码解析数据
        response.getWriter().write(htmlStr);//输出htmlStr里面的内容到客户端浏览器显示
    }

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

运行结果如下:

在服务器端使用getParameterNames方法接收表单参数,代码如下:

Enumeration<String> paramNames = request.getParameterNames();//获取所有的参数名
while (paramNames.hasMoreElements()) {
    String name = paramNames.nextElement();//得到参数名
    String value = request.getParameter(name);//通过参数名获取对应的值
    System.out.println(MessageFormat.format("{0}={1}", name,value));
}

运行结果如下:

在服务器端使用getParameterMap方法接收表单参数,代码如下:

//request对象封装的参数是以Map的形式存储的
Map<String, String[]> paramMap = request.getParameterMap();
for(Map.Entry<String, String[]> entry :paramMap.entrySet()){
    String paramName = entry.getKey();
    String paramValue = "";
    String[] paramValueArr = entry.getValue();
    for (int i = 0; paramValueArr!=null && i < paramValueArr.length; i++) {
        if (i == paramValueArr.length-1) {
            paramValue+=paramValueArr[i];
        }else {
            paramValue+=paramValueArr[i]+",";
        }
    }
    System.out.println(MessageFormat.format("{0}={1}", paramName,paramValue));
}

运行结果如下:

表单提交中文参数乱码问题

1、以POST方式提交表单中文参数的乱码问题

例如有如下的form表单页面:

<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
  <head>
    <title>request接收中文参数乱码问题</title>
  </head>

  <body>
      <form action="<%=request.getContextPath()%>/servlet/RequestDemo04" method="post">
          用户名:<input type="text" name="userName"/>
          <input type="submit" value="post方式提交表单">
      </form>
  </body>
</html>

此时在服务器端接收中文参数时就会出现中文乱码,如下所示:

2、post方式提交中文数据乱码产生的原因和解决办法

可以看到,之所以会产生乱码,就是因为服务器和客户端沟通的编码不一致造成的,因此解决的办法是:在客户端和服务器之间设置一个统一的编码,之后就按照此编码进行数据的传输和接收。

由于客户端是以UTF-8字符编码将表单数据传输到服务器端的,因此服务器也需要设置以UTF-8字符编码进行接收,要想完成此操作,服务器可以直接使用从ServletRequest接口继承而来的"setCharacterEncoding(charset)"方法进行统一的编码设置。修改后的代码如下:

public void doPost(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {
    /**
     * 客户端是以UTF-8编码传输数据到服务器端的,所以需要设置服务器端以UTF-8的编码进行接收,否则对于中文数据就会产生乱码
     */
    request.setCharacterEncoding("UTF-8");
    String userName = request.getParameter("userName");
    System.out.println("userName:"+userName);
}

使用request.setCharacterEncoding("UTF-8");设置服务器以UTF-8的编码接收数据后,此时就不会产生中文乱码问题了,如下所示:

3、以GET方式提交表单中文参数的乱码问题

例如有如下的form表单页面:

<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
  <head>
    <title>request接收中文参数乱码问题</title>
  </head>

  <body>
        <form action="${pageContext.request.contextPath}/servlet/RequestDemo04" method="get">
          姓名:<input type="text" name="name"/>
          <input type="submit" value="get方式提交表单">
      </form>
  </body>
</html>

此时在服务器端接收中文参数时就会出现中文乱码,如下所示:

那么这个中文乱码问题又该如何解决呢,是否可以通过request.setCharacterEncoding("UTF-8");设置服务器以UTF-8的编码进行接收这种方式来解决中文乱码问题呢,注意,对于以get方式传输的中文数据,通过request.setCharacterEncoding("UTF-8");这种方式是解决不了中文乱码问题,如下所示:

4、get方式提交中文数据乱码产生的原因和解决办法

对于以get方式传输的数据,request即使设置了以指定的编码接收数据也是无效的(至于为什么无效我也没有弄明白),默认的还是使用ISO8859-1这个字符编码来接收数据,客户端以UTF-8的编码传输数据到服务器端,而服务器端的request对象使用的是ISO8859-1这个字符编码来接收数据,服务器和客户端沟通的编码不一致因此才会产生中文乱码的。解决办法:在接收到数据后,先获取request对象以ISO8859-1字符编码接收到的原始数据的字节数组,然后通过字节数组以指定的编码构建字符串,解决乱码问题。代码如下:

public void doGet(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {
    /**
     *
     * 对于以get方式传输的数据,request即使设置了以指定的编码接收数据也是无效的,默认的还是使用ISO8859-1这个字符编码来接收数据
     */
    String name = request.getParameter("name");//接收数据
    name =new String(name.getBytes("ISO8859-1"), "UTF-8") ;//获取request对象以ISO8859-1字符编码接收到的原始数据的字节数组,然后通过字节数组以指定的编码构建字符串,解决乱码问题
    System.out.println("name:"+name);
}

运行结果如下:

5、以超链接形式传递中文参数的乱码问题

客户端想传输数据到服务器,可以通过表单提交的形式,也可以通过超链接后面加参数的形式,例如:

<a href="${pageContext.request.contextPath}/servlet/RequestDemo05?userName=gacl&name=徐达沛">点击</a>

点击超链接,数据是以get的方式传输到服务器的,所以接收中文数据时也会产生中文乱码问题,而解决中文乱码问题的方式与上述的以get方式提交表单中文数据乱码处理问题的方式一致,如下所示:

String name = request.getParameter("name");
name =new String(name.getBytes("ISO8859-1"), "UTF-8");

另外,需要提的一点就是URL地址后面如果跟了中文数据,那么中文参数最好使用URL编码进行处理,如下所示:

<a href="${pageContext.request.contextPath}/servlet/RequestDemo05?userName=gacl&name=<%=URLEncoder.encode("徐达沛", "UTF-8")%>">点击</a>

6、提交中文数据乱码问题总结

如果提交方式为post,想不乱码,只需要在服务器端设置request对象的编码即可,客户端以哪种编码提交的,服务器端的request对象就以对应的编码接收,比如客户端是以UTF-8编码提交的,那么服务器端request对象就以UTF-8编码接收(request.setCharacterEncoding("UTF-8"))

如果提交方式为get,设置request对象的编码是无效的,request对象还是以默认的ISO8859-1编码接收数据,因此要想不乱码,只能在接收到数据后再手工转换,步骤如下:

(1)获取获取客户端提交上来的数据,得到的是乱码字符串,data="???è?????" 

String data = request.getParameter("paramName"); 

(2)查找ISO8859-1码表,得到客户机提交的原始数据的字节数组

byte[] source = data.getBytes("ISO8859-1");

(3)通过字节数组以指定的编码构建字符串,解决乱码

data = new String(source, "UTF-8");

通过字节数组以指定的编码构建字符串,这里指定的编码是根据客户端那边提交数据时使用的字符编码来定的,如果是GB2312,那么就设置成data = new String(source, "GB2312"),如果是UTF-8,那么就设置成data = new String(source, "UTF-8")。

Request对象实现请求转发

1、请求转发的基本概念

请求转发:指一个web资源收到客户端请求后,通知服务器去调用另外一个web资源进行处理

请求转发的应用场景:MVC设计模式

在Servlet中实现请求转发的两种方式:

(1)通过ServletContext的getRequestDispatcher(String path)方法,该方法返回一个RequestDispatcher对象,调用这个对象的forward方法可以实现请求转发。

例如:将请求转发的test.jsp页面

RequestDispatcher reqDispatcher =this.getServletContext().getRequestDispatcher("/test.jsp");
reqDispatcher.forward(request, response);

(2)通过request对象提供的getRequestDispatche(String path)方法,该方法返回一个RequestDispatcher对象,调用这个对象的forward方法可以实现请求转发。

例如:将请求转发的test.jsp页面

request.getRequestDispatcher("/test.jsp").forward(request, response);

例如:请求RequestDemo06 Servlet,RequestDemo06将请求转发到test.jsp页面

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

public class RequestDemo06 extends HttpServlet {

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

        String data="大家好,我是孤傲苍狼,我正在总结JavaWeb";
        /**
         * 将数据存放到request对象中,此时把request对象当作一个Map容器来使用
         */
        request.setAttribute("data", data);
        //客户端访问RequestDemo06这个Servlet后,RequestDemo06通知服务器将请求转发(forward)到test.jsp页面进行处理
        request.getRequestDispatcher("/test.jsp").forward(request, response);
    }

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

test.jsp页面代码如下:

<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
  <head>
    <title>Request对象实现请求转发</title>
  </head>

  <body>
      使用普通方式取出存储在request对象中的数据:
      <h3 style="color:red;"><%=(String)request.getAttribute("data")%></h3>
     使用EL表达式取出存储在request对象中的数据:
     <h3 style="color:red;">${data}</h3>
  </body>
</html>

request对象同时也是一个域对象(Map容器),开发人员通过request对象在实现转发时,把数据通过request对象带给其它web资源处理。主要是通过以下的四个方法来操作:

  • setAttribute(String name,Object o)方法,将数据作为request对象的一个属性存放到request对象中,例如:request.setAttribute("data", data);
  • getAttribute(String name)方法,获取request对象的name属性的属性值,例如:request.getAttribute("data")
  • removeAttribute(String name)方法,移除request对象的name属性,例如:request.removeAttribute("data")
  • getAttributeNames方法,获取request对象的所有属性名,返回的是一个,例如:Enumeration<String> attrNames = request.getAttributeNames();

2、请求重定向和请求转发的区别

一个web资源收到客户端请求后,通知服务器去调用另外一个web资源进行处理,称之为请求转发/307。

一个web资源收到客户端请求后,通知浏览器去访问另外一个web资源进行处理,称之为请求重定向/302。

HttpServletResponse

Web服务器收到客户端的http请求,会针对每一次请求,分别创建一个用于代表请求的request对象、和代表响应的response对象。

request和response对象即然代表请求和响应,那我们要获取客户机提交过来的数据,只需要找request对象就行了。要向客户机输出数据,只需要找response对象就行了。 

常用方法

  • 负责向客户端(浏览器)发送数据的相关方法

  • 负责向客户端(浏览器)发送响应头的相关方法

  • 负责向客户端(浏览器)发送响应状态码的相关方法

常见应用

1、使用OutputStream流向客户端浏览器输出中文数据

使用OutputStream流输出中文注意问题:

在服务器端,数据是以哪个码表输出的,那么就要控制客户端浏览器以相应的码表打开,比如:outputStream.write("中国".getBytes("UTF-8"));使用OutputStream流向客户端浏览器输出中文,以UTF-8的编码进行输出,此时就要控制客户端浏览器以UTF-8的编码打开,否则显示的时候就会出现中文乱码,那么在服务器端如何控制客户端浏览器以以UTF-8的编码显示数据呢?可以通过设置响应头控制浏览器的行为,例如:response.setHeader("content-type", "text/html;charset=UTF-8");通过设置响应头控制浏览器以UTF-8的编码显示数据。

范例:使用OutputStream流向客户端浏览器输出"中国"这两个汉字

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

public class ResponseDemo01 extends HttpServlet {

    private static final long serialVersionUID = 4312868947607181532L;

    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        outputChineseByOutputStream(response);//使用OutputStream流输出中文
    }

    /**
     * 使用OutputStream流输出中文
     * @param request
     * @param response
     * @throws IOException
     */
    public void outputChineseByOutputStream(HttpServletResponse response) throws IOException{
        /**使用OutputStream输出中文注意问题:
         * 在服务器端,数据是以哪个码表输出的,那么就要控制客户端浏览器以相应的码表打开,
         * 比如:outputStream.write("中国".getBytes("UTF-8"));//使用OutputStream流向客户端浏览器输出中文,以UTF-8的编码进行输出
         * 此时就要控制客户端浏览器以UTF-8的编码打开,否则显示的时候就会出现中文乱码,那么在服务器端如何控制客户端浏览器以以UTF-8的编码显示数据呢?
         * 可以通过设置响应头控制浏览器的行为,例如:
         * response.setHeader("content-type", "text/html;charset=UTF-8");//通过设置响应头控制浏览器以UTF-8的编码显示数据
         */
        String data = "中国";
        OutputStream outputStream = response.getOutputStream();//获取OutputStream输出流
        response.setHeader("content-type", "text/html;charset=UTF-8");//通过设置响应头控制浏览器以UTF-8的编码显示数据,如果不加这句话,那么浏览器显示的将是乱码
        /**
         * data.getBytes()是一个将字符转换成字节数组的过程,这个过程中一定会去查码表,
         * 如果是中文的操作系统环境,默认就是查找查GB2312的码表,
         * 将字符转换成字节数组的过程就是将中文字符转换成GB2312的码表上对应的数字
         * 比如: "中"在GB2312的码表上对应的数字是98
         *         "国"在GB2312的码表上对应的数字是99
         */
        /**
         * getBytes()方法如果不带参数,那么就会根据操作系统的语言环境来选择转换码表,如果是中文操作系统,那么就使用GB2312的码表
         */
        byte[] dataByteArr = data.getBytes("UTF-8");//将字符转换成字节数组,指定以UTF-8编码进行转换
        outputStream.write(dataByteArr);//使用OutputStream流向客户端输出字节数组
    }

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

}

2、使用PrintWriter流向客户端浏览器输出中文数据

使用PrintWriter流输出中文注意问题:

在获取PrintWriter输出流之前首先使用"response.setCharacterEncoding(charset)"设置字符以什么样的编码输出到浏览器,如:response.setCharacterEncoding("UTF-8");设置将字符以"UTF-8"编码输出到客户端浏览器,然后再使用response.getWriter();获取PrintWriter输出流,这两个步骤不能颠倒,如下:

response.setCharacterEncoding("UTF-8");//设置将字符以"UTF-8"编码输出到客户端浏览器
/**
* PrintWriter out = response.getWriter();这句代码必须放在response.setCharacterEncoding("UTF-8");之后
* 否则response.setCharacterEncoding("UTF-8")这行代码的设置将无效,浏览器显示的时候还是乱码
*/
PrintWriter out = response.getWriter();//获取PrintWriter输出流

然后再使用response.setHeader("content-type", "text/html;charset=字符编码");设置响应头,控制浏览器以指定的字符编码编码进行显示,例如:

//通过设置响应头控制浏览器以UTF-8的编码显示数据,如果不加这句话,那么浏览器显示的将是乱码
response.setHeader("content-type", "text/html;charset=UTF-8");

除了可以使用response.setHeader("content-type", "text/html;charset=字符编码");设置响应头来控制浏览器以指定的字符编码编码进行显示这种方式之外,还可以用如下的方式来模拟响应头的作用

/**
* 多学一招:使用HTML语言里面的<meta>标签来控制浏览器行为,模拟通过设置响应头控制浏览器行为
 *response.getWriter().write("<meta http-equiv='content-type' content='text/html;charset=UTF-8'/>");
* 等同于response.setHeader("content-type", "text/html;charset=UTF-8");
*/
response.getWriter().write("<meta http-equiv='content-type' content='text/html;charset=UTF-8'/>");

范例:使用PrintWriter流向客户端浏览器输出"中国"这两个汉字

import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class ResponseDemo01 extends HttpServlet {

    private static final long serialVersionUID = 4312868947607181532L;

    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        outputChineseByPrintWriter(response);//使用PrintWriter流输出中文
    }

    /**
     * 使用PrintWriter流输出中文
     * @param request
     * @param response
     * @throws IOException
     */
    public void outputChineseByPrintWriter(HttpServletResponse response) throws IOException{
        String data = "中国";

        //通过设置响应头控制浏览器以UTF-8的编码显示数据,如果不加这句话,那么浏览器显示的将是乱码
        //response.setHeader("content-type", "text/html;charset=UTF-8");

        response.setCharacterEncoding("UTF-8");//设置将字符以"UTF-8"编码输出到客户端浏览器
        /**
         * PrintWriter out = response.getWriter();这句代码必须放在response.setCharacterEncoding("UTF-8");之后
         * 否则response.setCharacterEncoding("UTF-8")这行代码的设置将无效,浏览器显示的时候还是乱码
         */
        PrintWriter out = response.getWriter();//获取PrintWriter输出流
        /**
         * 多学一招:使用HTML语言里面的<meta>标签来控制浏览器行为,模拟通过设置响应头控制浏览器行为
         * out.write("<meta http-equiv='content-type' content='text/html;charset=UTF-8'/>");
         * 等同于response.setHeader("content-type", "text/html;charset=UTF-8");
         */
        out.write("<meta http-equiv='content-type' content='text/html;charset=UTF-8'/>");
        out.write(data);//使用PrintWriter流向客户端输出字符
    }

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

当需要向浏览器输出字符数据时,使用PrintWriter比较方便,省去了将字符转换成字节数组那一步。

3、使用OutputStream或者PrintWriter向客户端浏览器输出数字

比如有如下的代码:

import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class ResponseDemo01 extends HttpServlet {

    private static final long serialVersionUID = 4312868947607181532L;

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

        outputOneByOutputStream(response);//使用OutputStream输出1到客户端浏览器

    }

    /**
     * 使用OutputStream流输出数字1
     * @param request
     * @param response
     * @throws IOException
     */
    public void outputOneByOutputStream(HttpServletResponse response) throws IOException{
        response.setHeader("content-type", "text/html;charset=UTF-8");
        OutputStream outputStream = response.getOutputStream();
        outputStream.write("使用OutputStream流输出数字1:".getBytes("UTF-8"));
        outputStream.write(1);
    }

}

运行上面代码显示的结果如下:

运行的结果和我们想象中的不一样,数字1没有输出来,下面我们修改一下上面的outputOneByOutputStream方法的代码,修改后的代码如下:

/**
 * 使用OutputStream流输出数字1
 * @param request
 * @param response
 * @throws IOException
 */
public void outputOneByOutputStream(HttpServletResponse response) throws IOException{
    response.setHeader("content-type", "text/html;charset=UTF-8");
    OutputStream outputStream = response.getOutputStream();
    outputStream.write("使用OutputStream流输出数字1:".getBytes("UTF-8"));
    //outputStream.write(1);
    outputStream.write((1+"").getBytes());
}

1+""这一步是将数字1和一个空字符串相加,这样处理之后,数字1就变成了字符串1了,然后再将字符串1转换成字节数组使用OutputStream进行输出,此时看到的结果如下:

这次可以看到输出来的1了,这说明了一个问题:在开发过程中,如果希望服务器输出什么浏览器就能看到什么,那么在服务器端都要以字符串的形式进行输出。

如果使用PrintWriter流输出数字,那么也要先将数字转换成字符串后再输出,如下:

/**
 * 使用PrintWriter流输出数字1
 * @param request
 * @param response
 * @throws IOException
 */
public void outputOneByPrintWriter(HttpServletResponse response) throws IOException{
    response.setHeader("content-type", "text/html;charset=UTF-8");
    response.setCharacterEncoding("UTF-8");
    PrintWriter out = response.getWriter();//获取PrintWriter输出流
    out.write("使用PrintWriter流输出数字1:");
    out.write(1+"");
}

3、文件下载

文件下载功能是web开发中经常使用到的功能,使用HttpServletResponse对象就可以实现文件的下载。

文件下载功能的实现思路:

  • 获取要下载的文件的绝对路径
  • 获取要下载的文件名
  • 设置content-disposition响应头控制浏览器以下载的形式打开文件
  • 获取要下载的文件输入流
  • 创建数据缓冲区
  • 通过response对象获取OutputStream流
  • 将FileInputStream流写入到buffer缓冲区
  • 使用OutputStream将缓冲区的数据输出到客户端浏览器

范例:使用Response实现文件下载

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.URLEncoder;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
 * 文件下载
 */
public class ResponseDemo02 extends HttpServlet {

    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        downloadFileByOutputStream(response);//下载文件,通过OutputStream流
    }

    /**
     * 下载文件,通过OutputStream流
     * @param response
     * @throws FileNotFoundException
     * @throws IOException
     */
    private void downloadFileByOutputStream(HttpServletResponse response)
            throws FileNotFoundException, IOException {
        //1.获取要下载的文件的绝对路径
        String realPath = this.getServletContext().getRealPath("/download/1.JPG");
        //2.获取要下载的文件名
        String fileName = realPath.substring(realPath.lastIndexOf("\\")+1);
        //3.设置content-disposition响应头控制浏览器以下载的形式打开文件
        response.setHeader("content-disposition", "attachment;filename="+fileName);
        //4.获取要下载的文件输入流
        InputStream in = new FileInputStream(realPath);
        int len = 0;
        //5.创建数据缓冲区
        byte[] buffer = new byte[1024];
        //6.通过response对象获取OutputStream流
        OutputStream out = response.getOutputStream();
        //7.将FileInputStream流写入到buffer缓冲区
        while ((len = in.read(buffer)) > 0) {
        //8.使用OutputStream将缓冲区的数据输出到客户端浏览器
            out.write(buffer,0,len);
        }
        in.close();
    }

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

范例:使用Response实现中文文件下载

下载中文文件时,需要注意的地方就是中文文件名要使用URLEncoder.encode方法进行编码(URLEncoder.encode(fileName, "字符编码")),否则会出现文件名乱码。

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.URLEncoder;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
 * 文件下载
 */
public class ResponseDemo02 extends HttpServlet {

    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        downloadChineseFileByOutputStream(response);//下载中文文件
    }

    /**
     * 下载中文文件,中文文件下载时,文件名要经过URL编码,否则会出现文件名乱码
     * @param response
     * @throws FileNotFoundException
     * @throws IOException
     */
    private void downloadChineseFileByOutputStream(HttpServletResponse response)
            throws FileNotFoundException, IOException {
        String realPath = this.getServletContext().getRealPath("/download/张家界国家森林公园.JPG");//获取要下载的文件的绝对路径
        String fileName = realPath.substring(realPath.lastIndexOf("\\")+1);//获取要下载的文件名
        //设置content-disposition响应头控制浏览器以下载的形式打开文件,中文文件名要使用URLEncoder.encode方法进行编码,否则会出现文件名乱码
        response.setHeader("content-disposition", "attachment;filename="+URLEncoder.encode(fileName, "UTF-8"));
        InputStream in = new FileInputStream(realPath);//获取文件输入流
        int len = 0;
        byte[] buffer = new byte[1024];
        OutputStream out = response.getOutputStream();
        while ((len = in.read(buffer)) > 0) {
            out.write(buffer,0,len);//将缓冲区的数据输出到客户端浏览器
        }
        in.close();
    }

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

文件下载注意事项:编写文件下载功能时推荐使用OutputStream流,避免使用PrintWriter流,因为OutputStream流是字节流,可以处理任意类型的数据,而PrintWriter流是字符流,只能处理字符数据,如果用字符流处理字节数据,会导致数据丢失。

范例:使用PrintWriter流下载文件

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.URLEncoder;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
 * 文件下载
 */
public class ResponseDemo02 extends HttpServlet {

    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        downloadFileByPrintWriter(response);//下载文件,通过PrintWriter流
    }

    /**
     * 下载文件,通过PrintWriter流,虽然也能够实现下载,但是会导致数据丢失,因此不推荐使用PrintWriter流下载文件
     * @param response
     * @throws FileNotFoundException
     * @throws IOException
     */
    private void downloadFileByPrintWriter(HttpServletResponse response)
            throws FileNotFoundException, IOException {
        String realPath = this.getServletContext().getRealPath("/download/张家界国家森林公园.JPG");//获取要下载的文件的绝对路径
        String fileName = realPath.substring(realPath.lastIndexOf("\\")+1);//获取要下载的文件名
        //设置content-disposition响应头控制浏览器以下载的形式打开文件,中文文件名要使用URLEncoder.encode方法进行编码
        response.setHeader("content-disposition", "attachment;filename="+URLEncoder.encode(fileName, "UTF-8"));
        FileReader in = new FileReader(realPath);
        int len = 0;
        char[] buffer = new char[1024];
        PrintWriter out = response.getWriter();
        while ((len = in.read(buffer)) > 0) {
            out.write(buffer,0,len);//将缓冲区的数据输出到客户端浏览器
        }
        in.close();
    }

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

注意:

使用PrintWriter流处理字节数据,会导致数据丢失,这一点千万要注意,因此在编写下载文件功能时,要使用OutputStream流,避免使用PrintWriter流,因为OutputStream流是字节流,可以处理任意类型的数据,而PrintWriter流是字符流,只能处理字符数据,如果用字符流处理字节数据,会导致数据丢失。

4、设置响应头控制浏览器的行为

(1)设置http响应头控制浏览器禁止缓存当前文档内容    

response.setDateHeader("expries", -1);
response.setHeader("Cache-Control", "no-cache");
response.setHeader("Pragma", "no-cache");

(2)设置http响应头控制浏览器定时刷新网页(refresh)

response.setHeader("refresh", "5");//设置refresh响应头控制浏览器每隔5秒钟刷新一次

(3)通过response实现请求重定向

请求重定向指:一个web资源收到客户端请求后,通知客户端去访问另外一个web资源,这称之为请求重定向。

应用场景:用户登陆,用户首先访问登录页面,登录成功后,就会跳转到某个页面,这个过程就是一个请求重定向的过程

实现方式:response.sendRedirect(String location),即调用response对象的sendRedirect方法实现请求重定向

sendRedirect内部的实现原理:使用response设置302状态码和设置location响应头实现重定向

范例:

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

public class ResponseDemo04 extends HttpServlet {

    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        /**
         * 1.调用sendRedirect方法实现请求重定向,
         * sendRedirect方法内部调用了
         * response.setHeader("Location", "/JavaWeb_HttpServletResponse_Study_20140615/index.jsp");
         * response.setStatus(HttpServletResponse.SC_FOUND);//设置302状态码,等同于response.setStatus(302);
         */
        response.sendRedirect("/JavaWeb_HttpServletResponse_Study_20140615/index.jsp");

        //2.使用response设置302状态码和设置location响应头实现重定向实现请求重定向
        //response.setHeader("Location", "/JavaWeb_HttpServletResponse_Study_20140615/index.jsp");
        //response.setStatus(HttpServletResponse.SC_FOUND);//设置302状态码,等同于response.setStatus(302);
    }

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

response的细节问题

  • getOutputStream和getWriter方法分别用于得到输出二进制数据、输出文本数据的ServletOuputStream、Printwriter对象。
  • getOutputStream和getWriter这两个方法互相排斥,调用了其中的任何一个方法后,就不能再调用另一方法。 
  • Servlet程序向ServletOutputStream或PrintWriter对象中写入的数据将被Servlet引擎从response里面获取,Servlet引擎将这些数据当作响应消息的正文,然后再与响应状态行和各响应头组合后输出到客户端。 
  • Serlvet的service方法结束后,Servlet引擎将检查getWriter或getOutputStream方法返回的输出流对象是否已经调用过close方法,如果没有,Servlet引擎将调用close方法关闭该输出流对象。 

状态管理

Cookie

什么是cookie

cookie是web服务器存储在客户端硬盘上的一小段文本信息。这些信息存储后用于Web浏览器去检索来自相关计算机的信息。通常,cookie包含用于在不同网页之间通信的个性化用户数据和信息。

为什么使用cookie

cookie标识着计算机上用户的身份,并用于跟踪用户浏览整个网站页面的过程。我们知道Web浏览器和Web服务器之间的通信是无状态的。

比如,你访问域名http://www.example.com/1.html,Web浏览器将查询来自example.com网络服务器的1.html页面,下次当你输入网址http://www.example.com/2.html的时候,浏览器就会发送新的请求发给example.com网络服务器的2.html网页,而整个过程中Web服务器根本不知道前一页1.html已经提供过相关服务了。

如果你想让Web服务器知道该用户在前一段时间的通信历史,就需要在Web浏览器和Web服务器的交互过程中维持用户的状态信息,而这正是cookie作用的地方。因此,cookie服务的目的是维持Web服务器和用户间的交互。

cookie是怎么工作的

用来在网络上进行超文本信息传输的HTTP协议负责维护cookie。事实上HTTP协议有两种类型,无状态的HTTP协议和有状态HTTP协议。无状态HTTP协议不保留以前访问过的网页的历史记录。但是有状态HTTP协议将会保留Web浏览器和Web服务器交互的一些历史,有状态HTTP协议将使用cookie来维持用户交互。

当你访问一个使用cookie的网站或网页时,网页中的一小段代码(js、php和perl等脚本语言)将会把cookie写入到用户的机器中。

下面是用来写cookie的一个例子,它可以放置任何HTML网页内:

Set-Cookie:NAME=VALUE;expires=DATE;path=PATH;domain=DOMAIN_NAME;

当用户访问同一页面或同一域以后,这个cookie将会从磁盘中被读取并用于识别该域的同一个用户的第二次访问。另外需要解释的是cookie的过期时间设置,这个时间是由使用cookie的应用决定的。 

通常有两种类型的cookie会被写入用户的计算机上:

1)会话cookie:从打开浏览器开始,这个cookie就是活跃的,直到我们关闭浏览器,这个cookie才会被删除。有些时候应用程序也会将该cookie的过期时间设置为20分钟。

2)永久cookie:它是永久写入用户机器上的,有效期可以持续数月或数年。

cookie在哪里存储

任何网页应用程序写入的cookie都会被保存在用户本地硬盘的一个文本文件中。而且cookie的存储路径也与浏览器的设置有关。不同的浏览器会在不同的路径下存储cookie。例如IE浏览器存储cookie的路径为 “C:Users Default User AppDataRoamingMicrosoftWindowsCookies”,这里的“Default User”表示当前登录的用户,比如 “Administrator”等。

通过浏览器导航选项可以很容易的发现cookie的存储位置。在Mozilla Firefox浏览器的选项中,你甚至可以看到cookie的数据信息。具体操作为:打开Mozilla Firefox浏览器,点击工具- >选项- >隐私,然后点击“显示cookie”按钮。

cookie是怎么存储的

让我们来举一个例子,在Mozilla Firefox浏览器写下rediff.com的cookie,当在Mozilla Firefox浏览器打开网址rediff.com或登录rediff账户时,一个cookie就被写入到你的硬盘上。要查看此cookie只需找到上面提到的路径点击“显示Cookie”按钮。点击Rediff.com站点下这个cookie列表。你可以看到不同rediff域名写入的不同cookie。

cookie的应用场景

1)要实现购物车:你可以用来维持在线订购系统的一些状态,能够记住用户想要买什么。假设用户在他们的购物车增加了一些产品,但是由于某种原因,用户不希望购买这些产品,并关闭了浏览器窗口,而当下次相同的用户访问购买页面,他是可以看到自己上一次访问过程中在购物车添加的商品的。

2)个性化网站:用户在浏览网页时,根据个人偏好设置一些网页为不可见,或是将某些网站设置为不可访问,cookie将会记住用户的设置,下次用户在浏览网页的时候,cookie会根据用户的设置屏蔽一些网页。

3)用户追踪:要在特定的时间里,追踪一些特殊的访客。

4)营销:有些公司通过cookie在用户的机器上展示广告。cookie可以控制这些广告,什么时候、以什么样的形式被展示,同时也可以用来判断什么广告是用户感兴趣的,哪些关键字是用户曾在网站上搜索过的,所有这些东西都会记录在cookie中。

5)用户会话:cookie可以使用用户ID和密码在特定的域追踪用户的会话。

cookie的缺点

1)即使cookie是维持用户交互的好方法,但是如果用户设置浏览器为:写cookie警告或完全禁止cookie,那么包含cookie的网站将被完全禁用,用户将不能执行任何操作,从而造成网站流量的大量丢失。

2)cookie冗余:如果你在每一个网页导航中写太多cookie,用户打开浏览器选项:写cookie警告,这足可以使用户对你的网站敬而远之。

3)安全问题:用户的个人信息被存储在cookie中,如果有黑客劫持了用户的cookie,则黑客可以访问用户的个人信息。甚至别的域也可以读取一些损坏的cookie,从而带来安全问题。

4)敏感信息:有些网站可能在cookie中存储敏感信息,由于涉及隐私,这么做是禁止的。

Session

工作原理是什么

因为 http 协议是无状态的, 对于服务器端来讲, 如何为不同的访问用户提供不一样的体验呢? 比如邮箱系统, 只有登录用户才能收发邮件. 这就需要服务器能识别每一个客户端访问, 知道哪些访问是来自一个同一个客户端, 显然这个事情光靠服务器端是做不到的, 需要浏览器配合才行, 浏览器端 cookie 概念就这么产生了。

Session 工作原理:

(1)当一个 Session 第一次被启动时, 服务器端先产生一个唯一的 session id, 并为该 session id 分配一个内存区, 用来保存该 session 相关的信息. 然后将该 id 存储到浏览器的 cookie 中, cookie name 为 JSESSIONID。

(2)浏览器每次访问, 都会带上本域下所有的 cookie, 包括这个 JSESSIONID cookie(应该是放到 Http Header 上的), 服务器端通过对比 JSESSIONID 和服务器内存中的 session id, 就能识别哪些访问是来自同一个客户端。

(3)服务器端在每次访问中, 都可以在 JSESSIONID 对应的 session 内存区中记录一些信息, 比如最后一次的访问时间, 比如访问 IP 值, 以满足业务需求。

用途有哪些

1核心功能:是识别每一个访问

(2)基本功能:做登录验证, 然后就能得到登录 id, 进而能完成权限验证

核心功能和基本功能都由 Spring Web 框架提供, 或 Shiro 这样安全框架提供, 我们的代码不用直接处理这些事情。

(3)

 

 

 

 

 

JSESSIONID Cookie 是到底是在什么时候生成?

 

JSESSIONID的ID值会登录后会变化吗?

 

Session 和 cookie 之间的关系

 

Spring中Session和cookie的操作方式

 

ajax调用过程中是否会附加cookie信息

 

SSO/JWT/Session/OAuth2和web项目安全检查的关系

 

 

 

过滤器

过滤器是什么

  • 是向Web应用程序的请求和响应添加功能的Web服务组件
  • 过滤器可以统一的集中处理请求和响应
  • 使用过滤器技术实现对请求数据的过滤
  • 过滤器用于拦截传入的请求和传出的响应
  • 监视、修改或以某种方式处理正在客户端和服务器之间交换的数据流

使用过滤器封装公共任务具有如下优点:模块化;声明式;可重用;透明性

过滤器的常见用途

利用过滤器特殊的执行时机,可以实现 Web 应用程序中的预处理和后期处理逻辑。如:日志记录;改善性能;安全与会话管理;敏感字过滤。

过滤器的使用步骤

1、建立实现Filter接口的类(javax.servlet.Filter)

2、实现过滤行为:doFilter ( )

3、在web. xmI中配置过滤器

<filter>
    <filter-name>过滤器名</filter-name>
    <filter-class>过滤器的完全限定名</filter-class>
</filter>

<filter-mapping>
    <filter-name>过滤器名<filter-name>
    <url-pattern>过滤器映射的Web资源</url-pattern>
</filter-mapping>
<!--
    url-pattern指的是我过滤谁
    完全匹配:/index.jsp
    目录匹配:/admin/*
    扩展名匹配:*.do
    全部匹配:/*
-->

注意:Filter的启动会比Servlet早

过滤器的生命周期

  1. 实例化
  2. 初始化
  3. 过滤doFiIter( )
  4. 销毁destroy( )

Filter接口

javax.servlet.Filter接口定义了过滤器需要实现的方法。

  • void  init( FilterConfig  filterConfig):web容器调用该方法实现过滤器的初始化
  • void  doFilter(ServletRequest request,ServletResponse response,FilterChain chain ):当客户端请求资源时,Web容器会调用与资源对应的过滤器的doFilter( )方法。在该方法中,可以对请求和响应进行处理,实现过滤器的功能。
  • void  destroy( ):web容器销毁过滤器时调用该方法,可用来释放过滤器所用的资源

FilterConfig接口

在过滤器初始化过程中获取配置信息:

  • String  getFilterName():返回部署描述符中定义的过滤器的名称
  • Enumeration<String> getInitParameterNames():返回 <filter> 元素中定义的 init 参数的名称的字符串枚举
  • String  getInitParameter (String name):获取web.xml中设置的以name命名的初始化参数值
  • ServletContext  getServletContext():返回过滤器所属的Web上下文对象引用

配置和读取初始化参数

配置初始化参数

<web-app>
     <filter>
        <filter-name>CharacterEncodingFilter</filter-name>
        <filter-class>javaeedemo.filter.CharacterEncodingFilter</filter-class>
        <init-param>
            <param-name>charset</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
    </filter>
    ……
</web-app>

读取初始化参数

public class CharacterEncodingFilter implements Filter {
    private String charset = null;

    ……

    public void init(FilterConfig arg0) throws ServletException {
        String initParam = arg0.getInitParameter("charset");

              //读取指定名称的参数

        if (initParam != null && (initParam = initParam.trim()).length() != 0 ) {
            log.info("将CharacterEncodingFilter的charset设置为" + initParam);
            charset = initParam;

        }   
    }   
    ……   
}

过滤器链

  • 请求的URL与多个过滤器的映射范围相吻合时,这些过滤器会依次对请求进行过滤,形成过滤器链
  • 过滤器链的运行建立在嵌套调用的基础上
  • 可以根据业务需要,通过映射配置将过滤器链接组合起来,以提供更加全面的辅助功能

 

过滤器链中各过滤器之间是嵌套调用关系。

调用FilterChain对象的doFilter( )方法会导致链中的下一个过滤器被调用。如果正在执行的是链中最后一个过滤器,则目标资源将被调用。

过滤器链式排列的顺序由web.xml描述信息中<filter-mapping>元素的顺序决定,即先定义先执行。

Filter高级开发

在filter中可以得到代表用户请求和响应的request、response对象,因此在编程中可以使用Decorator(装饰器)模式对request、response对象进行包装,再把包装对象传给目标资源,从而实现一些特殊需求。

1、Decorator设计模式介绍

当某个对象的方法不适应业务需求时,通常有2种方式可以对方法进行增强:

  • 编写子类,覆盖需增强的方法。
  • 使用Decorator设计模式对方法进行增强。

装饰模式又名包装(Wrapper)模式。装饰模式以对客户端透明的方式扩展对象的功能,是继承关系的一个替代方案。装饰模式是在不必改变原类文件和使用继承的情况下,动态的扩展一个对象的功能。它是通过创建一个包装对象,也就是装饰来包裹真实的对象

在实际应用中遇到需增强对象的方法时,到底选用哪种方式比较好呢?这个没有具体的定式,只能是根据具体的需求来采用具体的方式,不过有一种情况下,必须使用Decorator设计模式:即被增强的对象,开发人员只能得到它的对象,无法得到它的class文件。比如request、response对象,开发人员之所以在servlet中能通过sun公司定义的HttpServletRequest\response接口去操作这些对象,是因为Tomcat服务器厂商编写了request、response接口的实现类。web服务器在调用servlet时,会用这些接口的实现类创建出对象,然后传递给servlet程序。此种情况下,由于开发人员根本不知道服务器厂商编写的request、response接口的实现类是哪个?在程序中只能拿到服务器厂商提供的对象,因此就只能采用Decorator设计模式对这些对象进行增强。

Decorator设计模式的实现步骤:

  1. 首先看需要被增强对象继承了什么接口或父类,编写一个类也去继承这些接口或父类。
  2. 在类中定义一个变量,变量类型即需增强对象的类型。
  3. 在类中定义一个构造函数,接收需增强的对象。
  4. 覆盖需增强的方法,编写增强的代码。

2、使用Decorator设计模式增强request对象

Servlet API 中提供了一个request对象的Decorator设计模式的默认实现类HttpServletRequestWrapper,HttpServletRequestWrapper 类实现了request 接口中的所有方法,但这些方法的内部实现都是仅仅调用了一下所包装的的 request 对象的对应方法,以避免用户在对request对象进行增强时需要实现request接口中的所有方法。

(1)使用Decorator模式包装request对象解决get和post请求方式下的中文乱码问题

编写一个用于处理中文乱码的过滤器CharacterEncodingFilter,代码如下:

import java.io.IOException;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;

/**
* @ClassName: CharacterEncodingFilter
* @Description: 此过滤器用来解决解决get、post请求方式下的中文乱码问题
*
*/
public class CharacterEncodingFilter implements Filter {

    private FilterConfig filterConfig = null;
    //设置默认的字符编码
    private String defaultCharset = "UTF-8";

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

        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) resp;
        //得到在web.xml中配置的字符编码
        String charset = filterConfig.getInitParameter("charset");
        if(charset==null){
            charset = defaultCharset;
        }
        request.setCharacterEncoding(charset);
        response.setCharacterEncoding(charset);
        response.setContentType("text/html;charset="+charset);

        MyCharacterEncodingRequest requestWrapper = new MyCharacterEncodingRequest(request);
        chain.doFilter(requestWrapper, response);
    }

    public void init(FilterConfig filterConfig) throws ServletException {
        //得到过滤器的初始化配置信息
        this.filterConfig = filterConfig;
    }

    public void destroy() {

    }
}

/**
* @ClassName: MyCharacterEncodingRequest
* @Description: Servlet API中提供了一个request对象的Decorator设计模式的默认实现类HttpServletRequestWrapper,
* (HttpServletRequestWrapper类实现了request接口中的所有方法,但这些方法的内部实现都是仅仅调用了一下所包装的的 request对象的对应方法)
* 以避免用户在对request对象进行增强时需要实现request接口中的所有方法。
* 所以当需要增强request对象时,只需要写一个类继承HttpServletRequestWrapper类,然后在重写需要增强的方法即可
*   1.实现与被增强对象相同的接口
    2、定义一个变量记住被增强对象
    3、定义一个构造函数,接收被增强对象
    4、覆盖需要增强的方法
    5、对于不想增强的方法,直接调用被增强对象(目标对象)的方法
*/
class MyCharacterEncodingRequest extends HttpServletRequestWrapper{
    //定义一个变量记住被增强对象(request对象是需要被增强的对象)
    private HttpServletRequest request;
    //定义一个构造函数,接收被增强对象
    public MyCharacterEncodingRequest(HttpServletRequest request) {
        super(request);
        this.request = request;
    }
    /* 覆盖需要增强的getParameter方法
     * @see javax.servlet.ServletRequestWrapper#getParameter(java.lang.String)
     */
    @Override
    public String getParameter(String name) {
        try{
            //获取参数的值
            String value= this.request.getParameter(name);
            if(value==null){
                return null;
            }
            //如果不是以get方式提交数据的,就直接返回获取到的值
            if(!this.request.getMethod().equalsIgnoreCase("get")) {
                return value;
            }else{
                //如果是以get方式提交数据的,就对获取到的值进行转码处理
                value = new String(value.getBytes("ISO8859-1"),this.request.getCharacterEncoding());
                return value;
            }
        }catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

在web.xml文件中配置CharacterEncodingFilter

<!--配置字符过滤器,解决get、post请求方式下的中文乱码问题-->
<filter>
    <filter-name>CharacterEncodingFilter</filter-name>
    <filter-class>me.gacl.web.filter.CharacterEncodingFilter</filter-class>
</filter>

<filter-mapping>
    <filter-name>CharacterEncodingFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

(2)使用Decorator模式包装request对象实现html标签转义功能

编写一个html转义过滤器,代码如下:

import java.io.IOException;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;

/**
* @ClassName: HtmlFilter
* @Description: html转义过滤器
*
*/
public class HtmlFilter implements Filter {

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

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

        MyHtmlRequest myrequest = new MyHtmlRequest(request);
        chain.doFilter(myrequest, response);

    }


    public void destroy() {

    }


    public void init(FilterConfig filterConfig) throws ServletException {

    }
}

/**
* @ClassName: MyHtmlRequest
* @Description: 使用Decorator模式包装request对象,实现html标签转义功能
*
*/
class MyHtmlRequest extends HttpServletRequestWrapper {

    private HttpServletRequest request;

    public MyHtmlRequest(HttpServletRequest request) {
        super(request);
        this.request = request;
    }

    /* 覆盖需要增强的getParameter方法
     * @see javax.servlet.ServletRequestWrapper#getParameter(java.lang.String)
     */
    @Override
    public String getParameter(String name) {
        String value = this.request.getParameter(name);
        if (value == null) {
            return null;
        }
        //调用filter转义value中的html标签
        return filter(value);
    }

    /**
    * @Method: filter
    * @Description: 过滤内容中的html标签
    * @Anthor:孤傲苍狼
    * @param message
    * @return
    */
    public String filter(String message) {
        if (message == null){
            return null;
        }
        char content[] = new char[message.length()];
        message.getChars(0, message.length(), content, 0);
        StringBuffer result = new StringBuffer(content.length + 50);
        for (int i = 0; i < content.length; i++) {
            switch (content[i]) {
            case '<':
                result.append("<");
                break;
            case '>':
                result.append(">");
                break;
            case '&':
                result.append("&");
                break;
            case '"':
                result.append(""");
                break;
            default:
                result.append(content[i]);
            }
        }
        return result.toString();
    }
}

在web.xml文件中配置HtmlFilter

<!--配置Html过滤器,转义内容中的html标签-->
<filter>
    <filter-name>HtmlFilter</filter-name>
    <filter-class>me.gacl.web.filter.HtmlFilter</filter-class>
</filter>

<filter-mapping>
    <filter-name>HtmlFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

(3)使用Decorator模式包装request对象实现敏感字符过滤功能

编写一个敏感字符过滤器,代码如下:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;

/**
* @ClassName: DirtyFilter
* @Description: 敏感词过滤器
*
*/
public class DirtyFilter implements Filter {

    private FilterConfig config = null;

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        this.config = filterConfig;
    }

    @Override
    public void doFilter(ServletRequest req, ServletResponse resp,
            FilterChain chain) throws IOException, ServletException {

        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) resp;
        DirtyRequest dirtyrequest = new DirtyRequest(request);

        chain.doFilter(dirtyrequest, response);
    }

    @Override
    public void destroy() {

    }

    /**
    * @Method: getDirtyWords
    * @Description: 获取敏感字符
    *
    * @return
    */
    private List<String> getDirtyWords(){
        List<String> dirtyWords = new ArrayList<String>();
        String dirtyWordPath = config.getInitParameter("dirtyWord");
        InputStream inputStream = config.getServletContext().getResourceAsStream(dirtyWordPath);
        InputStreamReader is = null;
        try {
            is = new InputStreamReader(inputStream,"UTF-8");
        } catch (UnsupportedEncodingException e2) {
            e2.printStackTrace();
        }
        BufferedReader reader = new BufferedReader(is);
        String line;
        try {
            while ((line = reader.readLine())!= null) {//如果 line为空说明读完了
                dirtyWords.add(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return dirtyWords;
    }

    /**
    * @ClassName: DirtyRequest
    * @Description: 使用Decorator模式包装request对象,实现敏感字符过滤功能
    *
    */
    class DirtyRequest extends HttpServletRequestWrapper{

        private List<String> dirtyWords = getDirtyWords();
        private HttpServletRequest request;
        public DirtyRequest(HttpServletRequest request) {
            super(request);
            this.request = request;
        }
        /* 重写getParameter方法,实现对敏感字符的过滤
         * @see javax.servlet.ServletRequestWrapper#getParameter(java.lang.String)
         */
        @Override
        public String getParameter(String name) {

            String value = this.request.getParameter(name);
            if(value==null){
                return null;
            }

            for(String dirtyWord : dirtyWords){
                if(value.contains(dirtyWord)){
                    System.out.println("内容中包含敏感词:"+dirtyWord+",将会被替换成****");
                    //替换敏感字符
                    value = value.replace(dirtyWord, "****");
                }
            }
            return value;
        }
    }
}

在web.xml文件中配置DirtyFilter:

<!--配置敏感字符过滤器-->
<filter>
    <filter-name>DirtyFilter</filter-name>
    <filter-class>me.gacl.web.filter.DirtyFilter</filter-class>
    <!-- 配置要过滤的敏感字符文件 -->
    <init-param>
        <param-name>dirtyWord</param-name>
        <param-value>/WEB-INF/DirtyWord.txt</param-value>
    </init-param>
</filter>

<filter-mapping>
    <filter-name>DirtyFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

当用户填写的内容包含一些敏感字符时,在DirtyFilter过滤器中就会将这些敏感字符替换掉。

我们如果将上述的CharacterEncodingFilter、HtmlFilter、DirtyFilter这三个过滤器联合起来使用,那么就相当于是把request对象包装了3次,request对象的getParameter方法经过3次重写,使得getParameter方法的功能大大增强,可以同时解决中文乱码,html标签转义,敏感字符过滤这些需求。

在实际开发中完全可以将上述的三个过滤器合并成一个,让合并后的过滤器具有解决中文乱码,html标签转义,敏感字符过滤这些功能,例如:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;

/**
* @ClassName: AdvancedFilter
* @Description: 这个过滤器是用来解决中文乱码,转义内容中的html标签,过滤内容中的敏感字符的
*
*/
public class AdvancedFilter implements Filter {

    private FilterConfig filterConfig = null;
    //设置默认的字符编码
    private String defaultCharset = "UTF-8";

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        //得到过滤器的初始化配置信息
        this.filterConfig = filterConfig;
    }

    @Override
    public void doFilter(ServletRequest req, ServletResponse resp,
            FilterChain chain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) resp;
        //得到在web.xml中配置的字符编码
        String charset = filterConfig.getInitParameter("charset");
        if(charset==null){
            charset = defaultCharset;
        }
        request.setCharacterEncoding(charset);
        response.setCharacterEncoding(charset);
        response.setContentType("text/html;charset="+charset);

        AdvancedRequest requestWrapper = new AdvancedRequest(request);
        chain.doFilter(requestWrapper, response);
    }

    @Override
    public void destroy() {

    }

    class AdvancedRequest extends HttpServletRequestWrapper{

        private List<String> dirtyWords = getDirtyWords();

        //定义一个变量记住被增强对象(request对象是需要被增强的对象)
        private HttpServletRequest request;
        //定义一个构造函数,接收被增强对象
        public AdvancedRequest(HttpServletRequest request) {
            super(request);
            this.request = request;
        }
        /* 覆盖需要增强的getParameter方法
         * @see javax.servlet.ServletRequestWrapper#getParameter(java.lang.String)
         */
        @Override
        public String getParameter(String name) {
            try{
                //获取参数的值
                String value= this.request.getParameter(name);
                if(value==null){
                    return null;
                }
                //如果不是以get方式提交数据的,就直接返回获取到的值
                if(!this.request.getMethod().equalsIgnoreCase("get")) {
                    //调用filter转义value中的html标签
                    value= filter(value);
                }else{
                    //如果是以get方式提交数据的,就对获取到的值进行转码处理
                    value = new String(value.getBytes("ISO8859-1"),this.request.getCharacterEncoding());
                    //调用filter转义value中的html标签
                    value= filter(value);
                }

                for(String dirtyWord : dirtyWords){
                    if(value.contains(dirtyWord)){
                        System.out.println("内容中包含敏感词:"+dirtyWord+",将会被替换成****");
                        //替换敏感字符
                        value = value.replace(dirtyWord, "****");
                    }
                }
                return value;
            }catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    }

    /**
    * @Method: filter
    * @Description: 过滤内容中的html标签
    * @param value
    * @return
    */
    public String filter(String value) {
        if (value == null){
            return null;
        }
        char content[] = new char[value.length()];
        value.getChars(0, value.length(), content, 0);
        StringBuffer result = new StringBuffer(content.length + 50);
        for (int i = 0; i < content.length; i++) {
            switch (content[i]) {
            case '<':
                result.append("<");
                break;
            case '>':
                result.append(">");
                break;
            case '&':
                result.append("&");
                break;
            case '"':
                result.append(""");
                break;
            default:
                result.append(content[i]);
            }
        }
        return (result.toString());
    }

    /**
    * @Method: getDirtyWords
    * @Description: 获取敏感字符
    *
    * @return
    */
    private List<String> getDirtyWords(){
        List<String> dirtyWords = new ArrayList<String>();
        String dirtyWordPath = filterConfig.getInitParameter("dirtyWord");
        InputStream inputStream = filterConfig.getServletContext().getResourceAsStream(dirtyWordPath);
        InputStreamReader is = null;
        try {
            is = new InputStreamReader(inputStream,defaultCharset);
        } catch (UnsupportedEncodingException e2) {
            e2.printStackTrace();
        }
        BufferedReader reader = new BufferedReader(is);
        String line;
        try {
            while ((line = reader.readLine())!= null) {//如果 line为空说明读完了
                dirtyWords.add(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return dirtyWords;
    }
}

在web.xml文件中配置AdvancedFilter

<filter>
    <filter-name>AdvancedFilter</filter-name>
    <filter-class>me.gacl.web.filter.AdvancedFilter</filter-class>
    <init-param>
        <param-name>charset</param-name>
        <param-value>UTF-8</param-value>
    </init-param>
    <init-param>
        <param-name>dirtyWord</param-name>
        <param-value>/WEB-INF/DirtyWord.txt</param-value>
    </init-param>
</filter>

<filter-mapping>
    <filter-name>AdvancedFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

AdvancedFilter过滤器同时具有解决中文乱码,转义内容中的html标签,过滤内容中的敏感字符这些功能。

3、使用Decorator设计模式增强response对象

Servlet  API 中提供了response对象的Decorator设计模式的默认实现类HttpServletResponseWrapper ,HttpServletResponseWrapper类实现了response接口中的所有方法,但这些方法的内部实现都是仅仅调用了一下所包装的的 response对象的对应方法,以避免用户在对response对象进行增强时需要实现response接口中的所有方法。

(1)response增强案例——压缩响应正文内容

应用HttpServletResponseWrapper对象,压缩响应正文内容。

具体思路:通过filter向目标页面传递一个自定义的response对象。在自定义的response对象中,重写getOutputStream方法和getWriter方法,使目标资源调用此方法输出页面内容时,获得的是我们自定义的ServletOutputStream对象。在我们自定义的ServletOuputStream对象中,重写write方法,使写出的数据写出到一个buffer中。当页面完成输出后,在filter中就可得到页面写出的数据,从而我们可以调用GzipOuputStream对数据进行压缩后再写出给浏览器,以此完成响应正文件压缩功能。

编写压缩过滤器,代码如下:

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.util.zip.GZIPOutputStream;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;

/**
* @ClassName: GzipFilter
* @Description: 压缩过滤器,将web应用中的文本都经过压缩后再输出到浏览器
*/
public class GzipFilter implements Filter {

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

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

        BufferResponse myresponse = new BufferResponse(response);
        chain.doFilter(request, myresponse);
        //拿出缓存中的数据,压缩后再打给浏览器
        byte out[] = myresponse.getBuffer();
        System.out.println("原始大小:" + out.length);

        ByteArrayOutputStream bout = new ByteArrayOutputStream();
        //压缩输出流中的数据
        GZIPOutputStream gout = new GZIPOutputStream(bout);
        gout.write(out);
        gout.close();

        byte gzip[] = bout.toByteArray();
        System.out.println("压缩后的大小:" + gzip.length);

        response.setHeader("content-encoding", "gzip");
        response.setContentLength(gzip.length);
        response.getOutputStream().write(gzip);
    }

    public void destroy() {

    }

    public void init(FilterConfig filterConfig) throws ServletException {

    }
}

class BufferResponse extends HttpServletResponseWrapper{

    private ByteArrayOutputStream bout = new ByteArrayOutputStream();
    private PrintWriter pw;
    private HttpServletResponse response;
    public BufferResponse(HttpServletResponse response) {
        super(response);
        this.response = response;
    }
    @Override
    public ServletOutputStream getOutputStream() throws IOException {
        return new MyServletOutputStream(bout);
    }
    @Override
    public PrintWriter getWriter() throws IOException {
        pw = new PrintWriter(new OutputStreamWriter(bout,this.response.getCharacterEncoding()));
        return pw;
    }

    public byte[] getBuffer(){
        try{
            if(pw!=null){
                pw.close();
            }
            if(bout!=null){
                bout.flush();
                return bout.toByteArray();
            }


            return null;
        }catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

class MyServletOutputStream extends ServletOutputStream{

    private ByteArrayOutputStream bout;
    public MyServletOutputStream(ByteArrayOutputStream bout){
        this.bout = bout;
    }

    @Override
    public void write(int b) throws IOException {
        this.bout.write(b);
    }
}

在web.xml中配置压缩过滤器

<filter>
    <description>配置压缩过滤器</description>
    <filter-name>GzipFilter</filter-name>
    <filter-class>me.gacl.web.filter.GzipFilter</filter-class>
</filter>

<!--jsp文件的输出的内容都经过压缩过滤器压缩后才输出 -->
<filter-mapping>
    <filter-name>GzipFilter</filter-name>
    <url-pattern>*.jsp</url-pattern>
    <!-- 配置过滤器的拦截方式-->
    <!-- 对于在Servlet中通过
          request.getRequestDispatcher("jsp页面路径").forward(request, response)
      方式访问的Jsp页面的要进行拦截 -->
    <dispatcher>FORWARD</dispatcher>
    <!--对于直接以URL方式访问的jsp页面进行拦截,过滤器的拦截方式默认就是 REQUEST-->
    <dispatcher>REQUEST</dispatcher>
</filter-mapping>
<!--js文件的输出的内容都经过压缩过滤器压缩后才输出 -->
<filter-mapping>
    <filter-name>GzipFilter</filter-name>
    <url-pattern>*.js</url-pattern>
</filter-mapping>
<!--css文件的输出的内容都经过压缩过滤器压缩后才输出 -->
<filter-mapping>
    <filter-name>GzipFilter</filter-name>
    <url-pattern>*.css</url-pattern>
</filter-mapping>
<!--html文件的输出的内容都经过压缩过滤器压缩后才输出 -->
<filter-mapping>
    <filter-name>GzipFilter</filter-name>
    <url-pattern>*.html</url-pattern>
</filter-mapping>

(2)response增强案例——缓存数据到内存

对于页面中很少更新的数据,例如商品分类,为避免每次都要从数据库查询分类数据,因此可把分类数据缓存在内存或文件中,以此来减轻数据库压力,提高系统响应速度。

编写缓存数据的过滤器,代码如下:

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;

/**
* @ClassName: WebResourceCachedFilter
* @Description: Web资源缓存过滤器
*
*/
public class WebResourceCachedFilter implements Filter {
    /**
    * @Field: map
    *          缓存Web资源的Map容器
    */
    private Map<String,byte[]> map = new HashMap<String,byte[]>();

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest req, ServletResponse resp,
            FilterChain chain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) resp;
        //1.得到用户请求的uri
        String uri = request.getRequestURI();
        //2.看缓存中有没有uri对应的数据
        byte b[] = map.get(uri);
        //3.如果缓存中有,直接拿缓存的数据打给浏览器,程序返回
        if(b!=null){
            //根据字节数组和指定的字符编码构建字符串
            String webResourceHtmlStr = new String(b,response.getCharacterEncoding());
            System.out.println(webResourceHtmlStr);
            response.getOutputStream().write(b);
            return;
        }
        //4.如果缓存没有,让目标资源执行,并捕获目标资源的输出
        BufferResponse myresponse = new BufferResponse(response);
        chain.doFilter(request, myresponse);
        //获取缓冲流中的内容的字节数组
        byte out[] = myresponse.getBuffer();
        //5.把资源的数据以用户请求的uri为关键字保存到缓存中
        map.put(uri, out);
        //6.把数据打给浏览器
        response.getOutputStream().write(out);
    }

    @Override
    public void destroy() {

    }

    class BufferResponse extends HttpServletResponseWrapper{
        private ByteArrayOutputStream bout = new ByteArrayOutputStream();  
        //捕获输出的缓存
        private PrintWriter pw;
        private HttpServletResponse response;
        public BufferResponse(HttpServletResponse response) {
            super(response);
            this.response = response;
        }
        @Override
        public ServletOutputStream getOutputStream() throws IOException {
            return new MyServletOutputStream(bout);
        }
        @Override
        public PrintWriter getWriter() throws IOException {
            pw = new PrintWriter(new OutputStreamWriter(bout,this.response.getCharacterEncoding()));
            return pw;
        }

        public byte[] getBuffer(){
            try{
                if(pw!=null){
                    pw.close();
                }
                return bout.toByteArray();
            }catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    }

    class MyServletOutputStream extends ServletOutputStream{
        private ByteArrayOutputStream bout;
        public MyServletOutputStream(ByteArrayOutputStream bout){  //接收数据写到哪里
            this.bout = bout;
        }
        @Override
        public void write(int b) throws IOException {
            bout.write(b);
        }
    }
}

在web.xml中配置Web资源缓存过滤器

<filter>
    <description>Web资源缓存过滤器</description>
    <filter-name>WebResourceCachedFilter</filter-name>
    <filter-class>me.gacl.web.filter.WebResourceCachedFilter</filter-class>
</filter>

<filter-mapping>
    <filter-name>WebResourceCachedFilter</filter-name>
    <!-- 映射需要缓存输出的JSP页面,这几个页面都只是单纯作为输入UI,不会有太多的变化,因此可以缓存输出 -->
    <url-pattern>/login.jsp</url-pattern>
    <url-pattern>/test.jsp</url-pattern>
    <url-pattern>/test2.jsp</url-pattern>
</filter-mapping>

过滤器示例

统一全站字符编码

通过配置参数charset指明使用何种字符编码,以处理Html Form请求参数的中文问题。

import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;

/**
* @ClassName: CharacterEncodingFilter
* @Description: 此过滤器用来解决全站中文乱码问题
*
*/
public class CharacterEncodingFilter implements Filter {

    private FilterConfig filterConfig = null;
    //设置默认的字符编码
    private String defaultCharset = "UTF-8";

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

        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) resp;
        String charset = filterConfig.getInitParameter("charset");
        if(charset==null){
            charset = defaultCharset;
        }
        request.setCharacterEncoding(charset);
        response.setCharacterEncoding(charset);
        response.setContentType("text/html;charset="+charset);

        MyCharacterEncodingRequest requestWrapper = new MyCharacterEncodingRequest(request);
        chain.doFilter(requestWrapper, response);
    }

    public void init(FilterConfig filterConfig) throws ServletException {
        //得到过滤器的初始化配置信息
        this.filterConfig = filterConfig;
    }

    public void destroy() {

    }
}

/*
1.实现与被增强对象相同的接口
2、定义一个变量记住被增强对象
3、定义一个构造器,接收被增强对象
4、覆盖需要增强的方法
5、对于不想增强的方法,直接调用被增强对象(目标对象)的方法
 */

class MyCharacterEncodingRequest extends HttpServletRequestWrapper{

    private HttpServletRequest request;
    public MyCharacterEncodingRequest(HttpServletRequest request) {
        super(request);
        this.request = request;
    }
    /* 重写getParameter方法
     * @see javax.servlet.ServletRequestWrapper#getParameter(java.lang.String)
     */
    @Override
    public String getParameter(String name) {

        try{
            //获取参数的值
            String value= this.request.getParameter(name);
            if(value==null){
                return null;
            }
            //如果不是以get方式提交数据的,就直接返回获取到的值
            if(!this.request.getMethod().equalsIgnoreCase("get")) {
                return value;
            }else{
                //如果是以get方式提交数据的,就对获取到的值进行转码处理
                value = new String(value.getBytes("ISO8859-1"),this.request.getCharacterEncoding());
                return value;
            }
        }catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

web.xml文件中的配置如下:

<filter>
  <filter-name>CharacterEncodingFilter</filter-name>
  <filter-class>me.gacl.web.filter.CharacterEncodingFilter</filter-class>
  <init-param>
      <param-name>charset</param-name>
      <param-value>UTF-8</param-value>
  </init-param>
</filter>

<filter-mapping>
  <filter-name>CharacterEncodingFilter</filter-name>
  <url-pattern>/*</url-pattern>
</filter-mapping>

禁止浏览器缓存所有动态页面

有3 个HTTP 响应头字段都可以禁止浏览器缓存当前页面,它们在 Servlet 中的示例代码如下:

response.setDateHeader("Expires",-1);
response.setHeader("Cache-Control","no-cache");
response.setHeader("Pragma","no-cache"); 

并不是所有的浏览器都能完全支持上面的三个响应头,因此最好是同时使用上面的三个响应头。

  • Expires数据头:值为GMT时间值,为-1指浏览器不要缓存页面
  • Cache-Control响应头有两个常用值:
    • no-cache指浏览器不要缓存当前页面。
    • max-age:xxx指浏览器缓存页面xxx秒。
import java.io.IOException;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
* @ClassName: NoCacheFilter
* @Description: 禁止浏览器缓存所有动态页面
*
*/
public class NoCacheFilter implements Filter {


    public void doFilter(ServletRequest req, ServletResponse resp,
            FilterChain chain) throws IOException, ServletException {
        //把ServletRequest强转成HttpServletRequest
        HttpServletRequest request = (HttpServletRequest) req;
        //把ServletResponse强转成HttpServletResponse
        HttpServletResponse response = (HttpServletResponse) resp;
        //禁止浏览器缓存所有动态页面
        response.setDateHeader("Expires", -1);
        response.setHeader("Cache-Control", "no-cache");
        response.setHeader("Pragma", "no-cache");

        chain.doFilter(request, response);
    }

    public void init(FilterConfig filterConfig) throws ServletException {

    }

    public void destroy() {

    }
}

web.xml文件中的配置如下:

<filter>
  <filter-name>NoCacheFilter</filter-name>
  <filter-class>me.gacl.web.filter.NoCacheFilter</filter-class>
</filter>

<filter-mapping>
  <filter-name>NoCacheFilter</filter-name>
    <!--只拦截Jsp请求-->
  <servlet-name>*.jsp</servlet-name>
</filter-mapping>

控制浏览器缓存页面中的静态资源

有些动态页面中引用了一些图片或css文件以修饰页面效果,这些图片和css文件经常是不变化的,所以为减轻服务器的压力,可以使用filter控制浏览器缓存这些文件,以提升服务器的性能。

import java.io.IOException;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
* @ClassName: CacheFilter
* @Description: 控制缓存的filter
*
*/
public class CacheFilter implements Filter {

    private FilterConfig filterConfig;

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

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

        //1.获取用户想访问的资源
        String uri = request.getRequestURI();

        //2.得到用户想访问的资源的后缀名
        String ext = uri.substring(uri.lastIndexOf(".")+1);

        //得到资源需要缓存的时间
        String time = filterConfig.getInitParameter(ext);
        if(time!=null){
            long t = Long.parseLong(time)*3600*1000;
            //设置缓存
            response.setDateHeader("expires", System.currentTimeMillis() + t);
        }

        chain.doFilter(request, response);

    }

    public void init(FilterConfig filterConfig) throws ServletException {
        this.filterConfig = filterConfig;
    }

    public void destroy() {

    }
}

web.xml文件中的配置如下:

<!-- 配置缓存过滤器 -->
<filter>
  <filter-name>CacheFilter</filter-name>
  <filter-class>me.gacl.web.filter.CacheFilter</filter-class>
   <!-- 配置要缓存的web资源以及缓存时间,以小时为单位 -->
  <init-param>
      <param-name>css</param-name>
      <param-value>4</param-value>
  </init-param>
  <init-param>
      <param-name>jpg</param-name>
      <param-value>1</param-value>
  </init-param>
  <init-param>
      <param-name>js</param-name>
      <param-value>4</param-value>
  </init-param>
  <init-param>
      <param-name>png</param-name>
      <param-value>4</param-value>
  </init-param>
</filter>
<!-- 配置要缓存的web资源的后缀-->
<filter-mapping>
  <filter-name>CacheFilter</filter-name>
  <url-pattern>*.jpg</url-pattern>
</filter-mapping>

<filter-mapping>
  <filter-name>CacheFilter</filter-name>
  <url-pattern>*.css</url-pattern>
</filter-mapping>

<filter-mapping>
  <filter-name>CacheFilter</filter-name>
  <url-pattern>*.js</url-pattern>
</filter-mapping>
<filter-mapping>
  <filter-name>CacheFilter</filter-name>
  <url-pattern>*.png</url-pattern>
</filter-mapping>

实现用户自动登陆

思路是这样的:

  • 在用户登陆成功后,发送一个名称为user的cookie给客户端,cookie的值为用户名和md5加密后的密码。
  • 编写一个AutoLoginFilter,这个filter检查用户是否带有名称为user的cookie来,如果有,则调用dao查询cookie的用户名和密码是否和数据库匹配,匹配则向session中存入user对象(即用户登陆标记),以实现程序完成自动登陆。

核心代码如下:

(1)处理用户登录的控制器:LoginServlet

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.harvey.dao.UserDao;
import com.harvey.domain.User;
import com.harvey.util.WebUtils;

public class LoginServlet extends HttpServlet {

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

        String username = request.getParameter("username");
        String password = request.getParameter("password");

        UserDao dao = new UserDao();
        User user = dao.find(username, password);
        if(user==null){
            request.setAttribute("message", "用户名或密码不对!!");
            request.getRequestDispatcher("/message.jsp").forward(request, response);
            return;
        }
        request.getSession().setAttribute("user", user);
        //发送自动登陆cookie给客户端浏览器进行存储
        sendAutoLoginCookie(request,response,user);
        request.getRequestDispatcher("/index.jsp").forward(request, response);
    }

    /**
    * @Method: sendAutoLoginCookie
    * @Description: 发送自动登录cookie给客户端浏览器
    *
    * @param request
    * @param response
    * @param user
    */
    private void sendAutoLoginCookie(HttpServletRequest request, HttpServletResponse response, User user) {
        if (request.getParameter("logintime")!=null) {
            int logintime = Integer.parseInt(request.getParameter("logintime"));
            //创建cookie,cookie的名字是autologin,值是用户登录的用户名和密码,用户名和密码之间使用.进行分割,密码经过md5加密处理
            Cookie cookie = new Cookie("autologin",user.getUsername() + "." + WebUtils.md5(user.getPassword()));
            //设置cookie的有效期
            cookie.setMaxAge(logintime);
            //设置cookie的有效路径
            cookie.setPath(request.getContextPath());
            //将cookie写入到客户端浏览器
            response.addCookie(cookie);
        }
    }

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

        doGet(request, response);
    }

}

(2)处理用户自动登录的过滤器:AutoLoginFilter

import java.io.IOException;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.harvey.dao.UserDao;
import com.harvey.domain.User;
import com.harvey.util.WebUtils;

public class AutoLoginFilter implements Filter {

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

        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) resp;
        //如果已经登录了,就直接chain.doFilter(request, response)放行
        if(request.getSession().getAttribute("user")!=null){
            chain.doFilter(request, response);
            return;
        }

        //1.得到用户带过来的authlogin的cookie
        String value = null;
        Cookie cookies[] = request.getCookies();
        for(int i=0;cookies!=null && i<cookies.length;i++){
            if(cookies[i].getName().equals("autologin")){
                value = cookies[i].getValue();
            }
        }

        //2.得到 cookie中的用户名和密码
        if(value!=null){
            String username = value.split("\\.")[0];
            String password = value.split("\\.")[1];

            //3.调用dao获取用户对应的密码
            UserDao dao = new UserDao();
            User user = dao.find(username);
            String dbpassword = user.getPassword();

            //4.检查用户带过来的md5的密码和数据库中的密码是否匹配,如匹配则自动登陆
            if(password.equals(WebUtils.md5(dbpassword))){
                request.getSession().setAttribute("user", user);
            }
        }

        chain.doFilter(request, response);
    }

    public void destroy() {

    }

    public void init(FilterConfig filterConfig) throws ServletException {

    }
}

(3)如果想取消自动登录,那么可以在用户注销时删除自动登录cookie,核心代码如下:

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class CancelAutoLoginServlet extends HttpServlet {

    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        //移除存储在session中的user
        request.getSession().removeAttribute("user");
        //移除自动登录的cookie
        removeAutoLoginCookie(request,response);
        //注销用户后跳转到登录页面
        request.getRequestDispatcher("/login.jsp").forward(request, response);
    }

    /**
    * @Method: removeAutoLoginCookie
    * @Description: 删除自动登录cookie,
    *     JavaWeb中删除cookie的方式就是新创建一个cookie,新创建的cookie与要删除的cookie同名,
    *     设置新创建的cookie的cookie的有效期设置为0,有效路径与要删除的cookie的有效路径相同
    *
    * @param request
    * @param response
    */
    private void removeAutoLoginCookie(HttpServletRequest request, HttpServletResponse response) {
        //创建一个名字为autologin的cookie
        Cookie cookie = new Cookie("autologin","");
         //将cookie的有效期设置为0,命令浏览器删除该cookie
        cookie.setMaxAge(0);
        //设置要删除的cookie的path
        cookie.setPath(request.getContextPath());
        response.addCookie(cookie);
    }

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

监听器

监听器是什么

  • Listener是Servlet的监听器
  • 监听客户端的请求和服务器端的操作
  • 通过实现Listener接口的类可以在特定事件(Event)发生时,自动激发一些操作
  • 监听器是Web应用程序事件模型的一部分
  • Web应用中的某些状态发生改变时会产生相应的事件
  • 监听器可以接收这些事件,以便在事件发生时做出相关处理

web的监听器主要是用来监听ServletContext、HttpSession、ServletRequest三个域对象。

常用的监听器接口

  • javax.servlet.ServletContextListener:在Servlet上下文对象初始化或销毁时得到通知
  • javax.servlet.ServletContextAttributeListener:在Servlet上下文中的属性列表发生变化时得到通知
  • javax.servlet.http.HttpSessionListener:在session创建后或者失效前得到通知
  • javax.servlet.http.HttpSessionActivationListener:绑定到session中,当session被钝化或者激活时得到通知
  • javax.servlet.http.HttpSessionAttributeListener:在session中的属性列表发生变化时得到通知
  • javax.servlet.http.HttpSessionBindingListener:在绑定session或从session中删除时会得到通知
  • javax.servlet.ServletRequestListener:在请求对象初始化时或者被销毁时得到通知
  • javax.servlet.ServletRequestAttributeListener:在请求对象中的属性列表发生变化时得到通知

监听器分类

根据三个域对象上的具体操作,将监听器划分为三类:

1、监听三个域对象的创建和销毁事件的监听器

  • javax.servlet.ServletContextListener:监听ServletContext对象的初始化与销毁事件
  • javax.servlet.http.HttpSessionListener:监听HttpSession对象的创建与销毁事件
  • javax.servlet.ServletRequestListener:监听ServletRequest对象的初始化与销毁事件,分别对应请求到达Web应用和离开Web应用

2、监听域对象中属性的增加、删除、替换事件的监听器

  • javax.servlet.ServletContextAttributeListener:监听application作用域中变量的增加、移除、替换事件
  • javax.servlet.http.HttpSessionAttributeListener:监听session作用域中变量的增加、移除、替换事件
  • javax.servlet.ServletRequestAttributeListener:监听request作用域中变量的增加、移除、替换事件

3、监听绑定到HttpSession域中的某个对象的状态的监听器,又称为感知型监听器

  • javax.servlet.http.HttpSessionBindingListener:其实现类的实例可以感知自己被绑定到session中或从session中解绑的事件
  • javax.servlet.http.HttpSessionActivationListener:其实现类的实例绑定到session中后,容器在钝化和活化session时将通知该实例

HttpSessionListener和HttpSessionAttributeListener可以对session作用域中的JavaBean实现统一的事件处理,感知型监听器则可以实现只对特定类型的JavaBean进行针对性的事件处理。

ServletContextListener

监听ServletContext对象的初始化与销毁事件;

常用于资源初始化加载、初始化创建等工作;

包含如下方法签名:

  • void contextInitialized( ServletContextEvent sce ):对应ServletContext对象的初始化事件;
  • void contextDestroyed( ServletContextEvent sce):对应ServletContext对象的销毁事件

参数javax.servlet.ServletContextEvent是代表Web应用上下文变化的事件类型,包含一个方法:

  • public ServletContext getServletContext( ):获得正在创建或销毁的ServletContent实例

HttpSessionListener

在WEB应用中,当一个session被创建或者销毁时启用这个监听器;session是一次会话。

关键点:

  • sessionCreated(HttpSessionEvent event):客户端第一次和服务器交互时候触发
  • sessionDestroyed(HttpSessionEventevent):销毁会话的时候触发
  • 必须在web.xmI中配置监听器
  • 监听范围:设置一次就可以监听所有session

session钝化与活化:

  • session钝化的本质就是把内存中的session对象序列化到存储设备中
  • 活化就是通过反序列化将session对象从存储设备上进行恢复
  • 可能用到钝化与活化的情况
    • 服务器资源不足
    • 重启服务器
    • Web应用被重新加载
  • session作用域中的变量只有实现了java.io. Serializable接口才能被钝化存储,否则将在钝化时被丢弃
  • 实现了HttpSessionActivationListener接口的JavaBean实例被添加到session后,能够感知session的钝化与活化事件,可以针对自身需要进行必要处理

HttpSessionBindingListener监听器和HttpSessionListener监听器的区别:

  • HttpSessionBindingListener不需要在web.xml里配置,是一对一的监听。
  • HttpSessionListener必须在web.xmI中配置,可以监听所有session。

HttpSessionAttributeListener

监听session作用域中变量的增加、移除、替换事件。

包含如下方法签名:

  • void attributeAdded(HttpSessionBindingEvent event ):对应变量被添加到session作用域的事件
  • void attributeRemoved(HttpSessionBindingEvent event ):对应session作用域中的变量被移除的事件
  • void attributeReplaced(HttpSessionBindingEvent event ):对应session作用域中的变量被替换的事件

参数javax.servlet.http.HttpSessionBindingEvent是代表session作用域中属性变化的事件类型,包含如下方法:

  • public HttpSession getSession():返回当前变化所对应的HttpSession对象
  • public String getName():返回发生变化的变量的名称
  • public Object getValue():对应不同的操作,可分别返回新添加的变量值、被移除的变量值、被替换的变量的旧值

HttpSessionActivationListener类型的监听器同样无须在web.xml文件中声明,将其实例放入session作用域即可。

HttpSessionBindingListener

其实例可感知自己与session作用域绑定和解绑的事件。

包含如下方法签名:

  • void valueBound(HttpSessionBindingEvent event ):对应监听器实例绑定到session作用域的事件
  • void valueUnbound(HttpSessionBindingEvent event ):对应监听器实例从session作用域解绑的事件

当一个实现了该接口的对象,被捆绑到session中或从session中被解放的时候启用此监听。

关键点:

  • 创建类实现HttpSessionBindingListener接口
  • valueBound ( )
  • valueUnbound( )
  • 不需要在web.xmI中配置监听器
  • 监听范围:一对一

监听器示例

监听器在JavaWeb开发中用得比较多,下面说一下监听器(Listener)在开发中的常见应用。

统计当前在线人数

在JavaWeb应用开发中,有时候我们需要统计当前在线的用户数,此时就可以使用监听器技术来实现这个功能了。

import javax.servlet.ServletContext;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;

/**
 * @ClassName: OnLineCountListener
 * @Description: 统计当前在线用户个数
 */
public class OnLineCountListener implements HttpSessionListener {

    @Override
    public void sessionCreated(HttpSessionEvent se) {
        ServletContext context = se.getSession().getServletContext();
        Integer onLineCount = (Integer) context.getAttribute("onLineCount");
        if (onLineCount == null) {
            context.setAttribute("onLineCount", 1);
        } else {
            onLineCount++;
            context.setAttribute("onLineCount", onLineCount);
        }
    }

    @Override
    public void sessionDestroyed(HttpSessionEvent se) {
        ServletContext context = se.getSession().getServletContext();
        Integer onLineCount = (Integer) context.getAttribute("onLineCount");
        if (onLineCount == null) {
            context.setAttribute("onLineCount", 1);
        } else {
            onLineCount--;
            context.setAttribute("onLineCount", onLineCount);
        }
    }
}

自定义Session扫描器

当一个Web应用创建的Session很多时,为了避免Session占用太多的内存,我们可以选择手动将这些内存中的session销毁,那么此时也可以借助监听器技术来实现。

import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Timer;
import java.util.TimerTask;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;

/**
* @ClassName: SessionScanerListener
* @Description: 自定义session扫描器
*
*/
public class SessionScanerListener implements HttpSessionListener,ServletContextListener {

    /**
    * @Field: list
    *          定义一个集合存储服务器创建的HttpSession
    *        LinkedList不是一个线程安全的集合
    */
    /**
     * private List<HttpSession> list = new LinkedList<HttpSession>();
     * 这样写涉及到线程安全问题,SessionScanerListener对象在内存中只有一个
     * sessionCreated可能会被多个人同时调用,
     * 当有多个人并发访问站点时,服务器同时为这些并发访问的人创建session
     * 那么sessionCreated方法在某一时刻内会被几个线程同时调用,几个线程并发调用sessionCreated方法
     * sessionCreated方法的内部处理是往一个集合中添加创建好的session,那么在加session的时候就会
     * 涉及到几个Session同时抢夺集合中一个位置的情况,所以往集合中添加session时,一定要保证集合是线程安全的才行
     * 如何把一个集合做成线程安全的集合呢?
     * 可以使用使用 Collections.synchronizedList(List<T> list)方法将不是线程安全的list集合包装线程安全的list集合
     */
    //使用 Collections.synchronizedList(List<T> list)方法将LinkedList包装成一个线程安全的集合
    private List<HttpSession> list = Collections.synchronizedList(new LinkedList<HttpSession>());
    //定义一个对象,让这个对象充当一把锁,用这把锁来保证往list集合添加的新的session和遍历list集合中的session这两个操作达到同步
    private Object lock = new Object();

    @Override
    public void sessionCreated(HttpSessionEvent se) {
        System.out.println("session被创建了!!");
        HttpSession session = se.getSession();

        synchronized (lock){
            /**
             *将该操作加锁进行锁定,当有一个thread-1(线程1)在调用这段代码时,会先拿到lock这把锁,然后往集合中添加session,
             *在添加session的这个过程中假设有另外一个thread-2(线程2)来访问了,thread-2可能是执行定时器任务的,
             *当thread-2要调用run方法遍历list集合中的session时,结果发现遍历list集合中的session的那段代码被锁住了,
             *而这把锁正在被往集合中添加session的那个thread-1占用着,因此thread-2只能等待thread-1操作完成之后才能够进行操作
             *当thread-1添加完session之后,就把lock放开了,此时thread-2拿到lock,就可以执行遍历list集合中的session的那段代码了
             *通过这把锁就保证了往集合中添加session和变量集合中的session这两步操作不能同时进行,必须按照先来后到的顺序来进行。
             */
            list.add(session);
        }
    }

    @Override
    public void sessionDestroyed(HttpSessionEvent se) {
        System.out.println("session被销毁了了!!");
    }

    /* Web应用启动时触发这个事件
     * @see javax.servlet.ServletContextListener#contextInitialized(javax.servlet.ServletContextEvent)
     */
    @Override
    public void contextInitialized(ServletContextEvent sce) {
        System.out.println("web应用初始化");
        //创建定时器
        Timer timer = new Timer();
        //每隔30秒就定时执行任务
        timer.schedule(new MyTask(list,lock), 0, 1000*30);
    }

    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        System.out.println("web应用关闭");
    }
}

/**
* @ClassName: MyTask
* @Description:定时器要定时执行的任务
*
*/
class MyTask extends TimerTask {

    //存储HttpSession的list集合
    private List<HttpSession> list;
    //存储传递过来的锁
    private Object lock;
    public MyTask(List<HttpSession> list,Object lock){
        this.list = list;
        this.lock = lock;
    }
    /* run方法指明了任务要做的事情
     * @see java.util.TimerTask#run()
     */
    @Override
    public void run() {
            //将该操作加锁进行锁定
        synchronized (lock) {
            System.out.println("定时器执行!!");
            ListIterator<HttpSession> it = list.listIterator();
            /**
             * 迭代list集合中的session,在迭代list集合中的session的过程中可能有别的用户来访问,
             * 用户一访问,服务器就会为该用户创建一个session,此时就会调用sessionCreated往list集合中添加新的session,
             * 然而定时器在定时执行扫描遍历list集合中的session时是无法知道正在遍历的list集合又添加的新的session进来了,
             * 这样就导致了往list集合添加的新的session和遍历list集合中的session这两个操作无法达到同步
             * 那么解决的办法就是把"list.add(session)和while(it.hasNext()){//迭代list集合}"这两段代码做成同步,
             * 保证当有一个线程在访问"list.add(session)"这段代码时,另一个线程就不能访问"while(it.hasNext()){//迭代list集合}"这段代码
             * 为了能够将这两段不相干的代码做成同步,只能定义一把锁(Object lock),然后给这两步操作加上同一把锁,
             * 用这把锁来保证往list集合添加的新的session和遍历list集合中的session这两个操作达到同步
             * 当在执行往list集合添加的新的session操作时,就必须等添加完成之后才能够对list集合进行迭代操作,
             * 当在执行对list集合进行迭代操作时,那么必须等到迭代操作结束之后才能够往往list集合添加的新的session
             */
            while(it.hasNext()){
                HttpSession session = (HttpSession) it.next();
                /**
                 * 如果当前时间-session的最后访问时间>1000*15(15秒)
                 * session.getLastAccessedTime()获取session的最后访问时间
                 */
                if(System.currentTimeMillis()-session.getLastAccessedTime()>1000*30){
                    //手动销毁session
                    session.invalidate();
                    //移除集合中已经被销毁的session
                    it.remove();
                }
            }
        }
    }
} 

模拟Servlet3.0注解方式配置Servlet

Servlet的传统配置方式

在JavaWeb开发中, 每次编写一个Servlet都需要在web.xml文件中进行配置,如下所示:

<servlet>
    <servlet-name>ActionServlet</servlet-name>
    <servlet-class>com.harvey.web.controller.ActionServlet</servlet-class>
</servlet>

<servlet-mapping>
    <servlet-name>ActionServlet</servlet-name>
    <url-pattern>/servlet/ActionServlet</url-pattern>
</servlet-mapping>

每开发一个Servlet,都要在web.xml中配置Servlet才能够使用,这实在是很头疼的事情,所以Servlet3.0之后提供了注解(annotation),使得不再需要在web.xml文件中进行Servlet的部署描述,简化开发流程。

本文所讲的基于注解方式配置Servlet不是针对Servlet3.0的,而是基于Servlet2.5的,通过开发自定义注解和注解处理器来实现类似于Servlet3.0的注解方式配置Servlet。

基于注解的方式配置Servlet

JDK1. 5版本之后, JAVA提供了一种叫做Annotation的新数据类型,中文译为注解或标注,它的出现为铺天盖地的XML配置文件提供了一个完美的解决方案,让 JAVA EE开发更加方便快速,也更加干净了。不过Servlet2.5默认情况下是不支持注解方式的配置的,但是我们可以开发自定义注解,然后将注解标注到Servlet上,再针对我们自定义的注解写一个注解处理器,具体的做法如下:

1、开发用于配置Servlet的相关注解

(1) 开发WebServlet注解,用于标注处理请求的Servlet类

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 自定义WebServlet注解,模拟Servlet3.0的WebServlet注解
 * @Target 注解的属性值表明了 @WebServlet注解只能用于类或接口定义声明的前面,
 * @WebServlet注解有一个必填的属性 value 。
 * 调用方式为: @WebServlet(value="/xxxx") ,
 * 因语法规定如果属性名为 value 且只填 value属性值时,可以省略 value属性名,即也可以写作:@WebServlet("/xxxx")
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface WebServlet {
    //Servlet的访问URL
    String value();
    //Servlet的访问URL
    String[] urlPatterns() default {""};
    //Servlet的描述
    String description() default "";
    //Servlet的显示名称
    String displayName() default "";
    //Servlet的名称
    String name() default "";
    //Servlet的init参数
    WebInitParam[] initParams() default {};
}

将Servlet在web.xml中的配置信息使用WebServlet注解来表示,使用注解后,只需要在相应Servlet 类的前面使用类似@WebServlet("/servlet/LoginServlet") 注解就可以达到和上述 web.xml 文件中配置信息一样的目的。

注解@WebServlet中的属性值"/servlet/LoginServlet"表示了web.xml 配置文件中 <servlet-mapping> 元素的子元素 <url-pattern> 里的值。通过这样的注解能简化在 XML 文件中配置 Servlet 信息,整个配置文件将会非常简洁干净,开发人员的工作也将大大减少。

(2)开发WebInitParam注解,用于配置Servlet初始化时使用的参数

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* @ClassName: WebInitParam
* @Description: 定义Servlet的初始化参数注解
*
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface WebInitParam {
    //参数名
    String paramName() default "";
    //参数的值
    String paramValue() default "";
}

2、编写处理注解的处理器

上面简要地介绍了注解的定义声明与使用方式,注解在后台需要一个处理器才能起作用,所以还得针对上面的注解编写处理器,在这里我们使用Filter作为注解的处理器,编写一个AnnotationHandleFilter,代码如下:

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
* @ClassName: AnnotationHandleFilter
* @Description: 使用Filter作为注解的处理器
*
*/
public class AnnotationHandleFilter implements Filter {

    private ServletContext servletContext = null;

    /* 过滤器初始化时扫描指定的包下面使用了WebServlet注解的那些类
     * @see javax.servlet.Filter#init(javax.servlet.FilterConfig)
     */
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("---AnnotationHandleFilter过滤器初始化开始---");
        servletContext = filterConfig.getServletContext();
        Map<String, Class<?>> classMap = new HashMap<String, Class<?>>();
        //获取web.xml中配置的要扫描的包
        String basePackage = filterConfig.getInitParameter("basePackage");
        //如果配置了多个包,例如:<param-value>me.gacl.web.controller,me.gacl.web.UI</param-value>
        if (basePackage.indexOf(",")>0) {
            //按逗号进行分隔
            String[] packageNameArr = basePackage.split(",");
            for (String packageName : packageNameArr) {
                addServletClassToServletContext(packageName,classMap);
            }
        }else {
            addServletClassToServletContext(basePackage,classMap);
        }
        System.out.println("----AnnotationHandleFilter过滤器初始化结束---");
    }

    /**
    * @Method: addServletClassToServletContext
    * @Description:添加ServletClass到ServletContext中
    * @Anthor:孤傲苍狼
    *
    * @param packageName
    * @param classMap
    */
    private void addServletClassToServletContext(String packageName,Map<String, Class<?>> classMap){
        Set<Class<?>> setClasses =  ScanClassUtil.getClasses(packageName);
        for (Class<?> clazz :setClasses) {
            if (clazz.isAnnotationPresent(WebServlet.class)) {
                //获取WebServlet这个Annotation的实例
                WebServlet annotationInstance = clazz.getAnnotation(WebServlet.class);
                //获取Annotation的实例的value属性的值
                String annotationAttrValue = annotationInstance.value();
                if (!annotationAttrValue.equals("")) {
                    classMap.put(annotationAttrValue, clazz);
                }
                //获取Annotation的实例的urlPatterns属性的值
                String[] urlPatterns = annotationInstance.urlPatterns();
                for (String urlPattern : urlPatterns) {
                    classMap.put(urlPattern, clazz);
                }
                servletContext.setAttribute("servletClassMap", classMap);
                System.out.println("annotationAttrValue:"+annotationAttrValue);
                String targetClassName = annotationAttrValue.substring(annotationAttrValue.lastIndexOf("/")+1);
                System.out.println("targetClassName:"+targetClassName);
                System.out.println(clazz);
            }
        }
    }

    public void doFilter(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException {
        System.out.println("---进入注解处理过滤器---");
        //将ServletRequest强制转换成HttpServletRequest
        HttpServletRequest req = (HttpServletRequest)request;
        HttpServletResponse res = (HttpServletResponse)response;
        Map<String, Class<?>> classMap = (Map<String, Class<?>>) servletContext.getAttribute("servletClassMap");
        //获取contextPath
        String contextPath = req.getContextPath();
        //获取用户请求的URI资源
        String uri = req.getRequestURI();
        //如果没有指明要调用Servlet类中的哪个方法
        if (uri.indexOf("!")==-1) {
            //获取用户使用的请求方式
            String reqMethod = req.getMethod();
            //获取要请求的servlet路径
            String requestServletName = uri.substring(contextPath.length(),uri.lastIndexOf("."));
            //获取要使用的类
            Class<?> clazz = classMap.get(requestServletName);
            //创建类的实例
            Object obj = null;
            try {
                obj = clazz.newInstance();
            } catch (InstantiationException e1) {
                e1.printStackTrace();
            } catch (IllegalAccessException e1) {
                e1.printStackTrace();
            }
            Method targetMethod = null;
            if (reqMethod.equalsIgnoreCase("get")) {
                try {
                     targetMethod = clazz.getDeclaredMethod("doGet",HttpServletRequest.class,HttpServletResponse.class);
                } catch (SecurityException e) {
                    e.printStackTrace();
                } catch (NoSuchMethodException e) {
                    e.printStackTrace();
                }
            }else {
                try {
                    targetMethod = clazz.getDeclaredMethod("doPost",HttpServletRequest.class,HttpServletResponse.class);
                } catch (SecurityException e) {
                    e.printStackTrace();
                } catch (NoSuchMethodException e) {
                    e.printStackTrace();
                }
            }

            try {
                //调用对象的方法进行处理
                targetMethod.invoke(obj,req,res);
            } catch (IllegalArgumentException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
        }else {
            //获取要请求的servlet路径
            String requestServletName = uri.substring(contextPath.length(),uri.lastIndexOf("!"));
            //获取要调用的servlet的方法
            String invokeMethodName = uri.substring(uri.lastIndexOf("!")+1,uri.lastIndexOf("."));

            //获取要使用的类
            Class<?> clazz = classMap.get(requestServletName);
            //创建类的实例
            Object obj = null;
            try {
                obj = clazz.newInstance();
            } catch (InstantiationException e1) {
                e1.printStackTrace();
            } catch (IllegalAccessException e1) {
                e1.printStackTrace();
            }
            //获得clazz类定义的所有方法
            Method[] methods = clazz.getDeclaredMethods();
            //获取WebServlet这个Annotation的实例
            WebServlet annotationInstance = clazz.getAnnotation(WebServlet.class);
            //获取注解上配置的初始化参数数组
            WebInitParam[] initParamArr = annotationInstance.initParams();
            Map<String, String> initParamMap = new HashMap<String, String>();
            for (WebInitParam initParam : initParamArr) {
                initParamMap.put(initParam.paramName(), initParam.paramValue());
            }
            //遍历clazz类中的方法
            for (Method method : methods) {
                //该方法的返回类型
                Class<?> retType = method.getReturnType();
                //获得方法名
                String methodName = method.getName();
                //打印方法修饰符
                System.out.print(Modifier.toString(method.getModifiers()));
                System.out.print(" "+retType.getName() + " " + methodName +"(");
                //获得一个方法参数数组(getparameterTypes用于返回一个描述参数类型的Class对象数组)
                Class<?>[] paramTypes = method.getParameterTypes();
                for(int j = 0 ; j < paramTypes.length ; j++){
                     //如果有多个参数,中间则用逗号隔开,否则直接打印参数
                    if (j > 0){
                        System.out.print(",");
                    }
                    System.out.print(paramTypes[j].getName());
                }
                System.out.println(");");
                if (method.getName().equalsIgnoreCase("init")) {
                    try {
                        //调用Servlet的初始化方法
                        method.invoke(obj, initParamMap);
                    } catch (IllegalArgumentException e) {
                        e.printStackTrace();
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    } catch (InvocationTargetException e) {
                        e.printStackTrace();
                    }
                }
            }
            //获取WebServlet这个Annotation的实例
            System.out.println("invokeMethodName:"+invokeMethodName);
            try {
                try {
                    //利用反射获取方法实例,方法的签名必须符合:
                    //public void 方法名(HttpServletRequest request, HttpServletResponse response)
                    //例如:public void loginHandle(HttpServletRequest request, HttpServletResponse response)
                    Method targetMethod = clazz.getDeclaredMethod(invokeMethodName,HttpServletRequest.class,HttpServletResponse.class);
                    //调用对象的方法进行处理
                    targetMethod.invoke(obj,req,res);
                } catch (SecurityException e) {
                    e.printStackTrace();
                } catch (NoSuchMethodException e) {
                    e.printStackTrace();
                } catch (IllegalArgumentException e) {
                    e.printStackTrace();
                } catch (InvocationTargetException e) {
                    e.printStackTrace();
                }
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
    }

    public void destroy() {

    }
}

AnnotationHandleFilter过滤器初始化时扫描指定的包下面使用了WebServlet注解的那些类,然后将类存储到一个Map集合中,再将Map集合存储到servletContext对象中。

在web.xml文件中配置AnnotationHandleFilter过滤器和需要扫描的包

<filter>
    <description>注解处理过滤器</description>
    <filter-name>AnnotationHandleFilter</filter-name>
    <filter-class>me.gacl.web.filter.AnnotationHandleFilter</filter-class>
    <init-param>
        <description>配置要扫描包及其子包, 如果有多个包,以逗号分隔</description>
        <param-name>basePackage</param-name>
        <param-value>me.gacl.web.controller,me.gacl.web.UI</param-value>
        <!-- <param-value>me.gacl.web.controller</param-value> -->
    </init-param>
</filter>

<filter-mapping>
    <filter-name>AnnotationHandleFilter</filter-name>
    <!-- 拦截后缀是.do的请求 -->
    <url-pattern>*.do</url-pattern>
</filter-mapping>

AnnotationHandleFilter过滤器初始化方法init(FilterConfig filterConfig)使用到了一个用于扫描某个包下面的类的工具类ScanClassUtil,ScanClassUtil的代码如下:

import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.net.JarURLConnection;
import java.net.URL;
import java.net.URLDecoder;
import java.util.Enumeration;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

public class ScanClassUtil {

    /**
     * 从包package中获取所有的Class
     *
     * @param pack
     * @return
     */
    public static Set<Class<?>> getClasses(String pack) {

        // 第一个class类的集合
        Set<Class<?>> classes = new LinkedHashSet<Class<?>>();
        // 是否循环迭代
        boolean recursive = true;
        // 获取包的名字 并进行替换
        String packageName = pack;
        String packageDirName = packageName.replace('.', '/');
        // 定义一个枚举的集合 并进行循环来处理这个目录下的things
        Enumeration<URL> dirs;
        try {
            dirs = Thread.currentThread().getContextClassLoader().getResources(
                    packageDirName);
            // 循环迭代下去
            while (dirs.hasMoreElements()) {
                // 获取下一个元素
                URL url = dirs.nextElement();
                // 得到协议的名称
                String protocol = url.getProtocol();
                // 如果是以文件的形式保存在服务器上
                if ("file".equals(protocol)) {
                    System.err.println("file类型的扫描");
                    // 获取包的物理路径
                    String filePath = URLDecoder.decode(url.getFile(), "UTF-8");
                    // 以文件的方式扫描整个包下的文件 并添加到集合中
                    findAndAddClassesInPackageByFile(packageName, filePath,
                            recursive, classes);
                } else if ("jar".equals(protocol)) {
                    // 如果是jar包文件
                    // 定义一个JarFile
                    System.err.println("jar类型的扫描");
                    JarFile jar;
                    try {
                        // 获取jar
                        jar = ((JarURLConnection) url.openConnection())
                                .getJarFile();
                        // 从此jar包 得到一个枚举类
                        Enumeration<JarEntry> entries = jar.entries();
                        // 同样的进行循环迭代
                        while (entries.hasMoreElements()) {
                            // 获取jar里的一个实体 可以是目录 和一些jar包里的其他文件 如META-INF等文件
                            JarEntry entry = entries.nextElement();
                            String name = entry.getName();
                            // 如果是以/开头的
                            if (name.charAt(0) == '/') {
                                // 获取后面的字符串
                                name = name.substring(1);
                            }
                            // 如果前半部分和定义的包名相同
                            if (name.startsWith(packageDirName)) {
                                int idx = name.lastIndexOf('/');
                                // 如果以"/"结尾 是一个包
                                if (idx != -1) {
                                    // 获取包名 把"/"替换成"."
                                    packageName = name.substring(0, idx)
                                            .replace('/', '.');
                                }
                                // 如果可以迭代下去 并且是一个包
                                if ((idx != -1) || recursive) {
                                    // 如果是一个.class文件 而且不是目录
                                    if (name.endsWith(".class")
                                            && !entry.isDirectory()) {
                                        // 去掉后面的".class" 获取真正的类名
                                        String className = name.substring(
                                                packageName.length() + 1, name
                                                        .length() - 6);
                                        try {
                                            // 添加到classes
                                            classes.add(Class
                                                    .forName(packageName + '.'
                                                            + className));
                                        } catch (ClassNotFoundException e) {
                                            // log
                                            // .error("添加用户自定义视图类错误 找不到此类的.class文件");
                                            e.printStackTrace();
                                        }
                                    }
                                }
                            }
                        }
                    } catch (IOException e) {
                        // log.error("在扫描用户定义视图时从jar包获取文件出错");
                        e.printStackTrace();
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

        return classes;
    }

    /**
     * 以文件的形式来获取包下的所有Class
     *
     * @param packageName
     * @param packagePath
     * @param recursive
     * @param classes
     */
    public static void findAndAddClassesInPackageByFile(String packageName,
            String packagePath, final boolean recursive, Set<Class<?>> classes) {
        // 获取此包的目录 建立一个File
        File dir = new File(packagePath);
        // 如果不存在或者 也不是目录就直接返回
        if (!dir.exists() || !dir.isDirectory()) {
            // log.warn("用户定义包名 " + packageName + " 下没有任何文件");
            return;
        }
        // 如果存在 就获取包下的所有文件 包括目录
        File[] dirfiles = dir.listFiles(new FileFilter() {
            // 自定义过滤规则 如果可以循环(包含子目录) 或则是以.class结尾的文件(编译好的java类文件)
            public boolean accept(File file) {
                return (recursive && file.isDirectory())
                        || (file.getName().endsWith(".class"));
            }
        });
        // 循环所有文件
        for (File file : dirfiles) {
            // 如果是目录 则继续扫描
            if (file.isDirectory()) {
                findAndAddClassesInPackageByFile(packageName + "."
                        + file.getName(), file.getAbsolutePath(), recursive,
                        classes);
            } else {
                // 如果是java类文件 去掉后面的.class 只留下类名
                String className = file.getName().substring(0,
                        file.getName().length() - 6);
                try {
                    // 添加到集合中去
                    //classes.add(Class.forName(packageName + '.' + className));
                     //经过回复同学的提醒,这里用forName有一些不好,会触发static方法,没有使用classLoader的load干净
                    classes.add(Thread.currentThread().getContextClassLoader().loadClass(packageName + '.' + className));
                    } catch (ClassNotFoundException e) {
                    // log.error("添加用户自定义视图类错误 找不到此类的.class文件");
                    e.printStackTrace();
                }
            }
        }
    }
}

经过以上两步,我们的自定义注解和针对注解的处理器都开发好了。

3、WebServlet注解简单测试

编写一个用于跳转到Login.jsp页面的LoginUIServlet,LoginUIServlet就是一个普通的java类,不是一个真正的Servlet,然后使用WebServlet注解标注LoginUIServlet类,代码如下:

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

@WebServlet("/servlet/LoginUI")
public class LoginUIServlet {

    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException{
        request.getRequestDispatcher("/Login.jsp").forward(request, response);
    }

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

在浏览器中输入访问地址:http://localhost:8080/AnnotationConfigServlet/servlet/Login.do,根据web.xml文件的配置,所有后缀名为 .do请求,都会经过AnnotationHandleFilter过滤器的doFilter方法,在doFilter方法的实现代码中,从HttpServletRequest请求对象中得到请求的方式类型(GET/POST)和请求的URI 。

如有请求http://localhost:8080/AnnotationConfigServlet/servlet/LoginUI.do,此时请求方法类型为GET, URI 值为/AnnotationConfigServlet/servlet/LoginUI.do。从ServletConext对象中获取到在过滤器中保存的Map结构,根据 URI 获得一个 Key=”/servlet/LoginUI” ,从 Map 结构中根据此Key得到Value ,此时Value就是要请求调用的那个Servlet类,根据Servlet类创建对象实例,再根据前面得到的请求方法类型,能决定调用此Servlet对象实例的 doGet 或 doPost 方法。最终客户端发生的后缀为.do请求,经由AnnotationHandleFilter对请求对象(HttpServletRequest)的分析,从而调用相应某Servlet的doGet或doPost方法,完成了一次客户端请求到服务器响应的过程。

使用注解后程序流程如下所示:

Servlet3.0是支持采用基于注解的方式配置Servlet的,在此我使用过滤器作为注解处理器模拟模拟出了类似Servlet3.0的注解处理方式,简化了Servlet的配置。这种使用自定义注解+注解处理器的方式山寨出来的Servlet3.0大家了解一下即可,了解一下这种处理思路,在实际应用中还是不要这么做了,要真想使用注解的方式配置Servlet还是直接用Servlet3.0吧。

web.xml配置文件

启动一个WEB项目的时候,WEB容器会去读取它的配置文件web.xml,读取<listener>和<context-param>两个结点。 

紧接着,容器会创建一个ServletContext(servlet上下文),这个web项目的所有部分都将共享这个上下文。 

容器将<context-param>转换为键值对,并交给servletContext。

容器创建<listener>中的类实例,创建监听器。 

load-on-startup

load-on-startup 元素在web应用启动的时候指定了servlet被加载的顺序,它的值必须是一个整数。如果它的值是一个负整数或是这个元素不存在,那么容器会在该servlet被调用的时候,加载这个servlet 。如果值是正整数或零,容器在配置的时候就加载并初始化这个servlet,容器必须保证值小的先被加载。如果值相等,容器可以自动选择先加载谁。  

在servlet的配置当中,<load-on-startup>5</load-on-startup>的含义是: 

  • 标记容器是否在启动的时候就加载这个servlet。 
  • 当值为0或者大于0时,表示容器在应用启动时就加载这个servlet; 
  • 当是一个负数时或者没有指定时,则指示容器在该servlet被选择时才加载。 
  • 正数的值越小,启动该servlet的优先级越高。 

加载顺序

首先可以肯定的是,加载顺序与它们在 web.xml 文件中的先后顺序无关。即不会因为 filter 写在 listener 的前面而会先加载 filter。

最终得出的结论是:ServletContext -> listener -> filter -> servlet

同时还存在着这样一种配置节:context-param,它用于向 ServletContext 提供键值对,即应用程序上下文信息。我们的 listener, filter 等在初始化时会用到这些上下文中的信息,那么 context-param 配置节是不是应该写在 listener 配置节前呢?实际上 context-param 配置节可写在任意位置,因此真正的加载顺序为:context-param -> listener -> filter -> servlet

对于某类配置节而言,与它们出现的顺序是有关的。以 filter 为例,web.xml 中当然可以定义多个 filter,与 filter 相关的一个配置节是 filter-mapping,这里一定要注意,对于拥有相同 filter-name 的 filter 和 filter-mapping 配置节而言,filter-mapping 必须出现在 filter 之后,否则当解析到 filter-mapping 时,它所对应的 filter-name 还未定义。web 容器启动时初始化每个 filter 时,是按照 filter 配置节出现的顺序来初始化的,当请求资源匹配多个 filter-mapping 时,filter 拦截资源是按照 filter-mapping 配置节出现的顺序来依次调用 doFilter() 方法的。

servlet 同 filter 类似,此处不再赘述。

由此,可以看出,web.xml 的加载顺序是:ServletContext -> context-param -> listener -> filter -> servlet ,而同个类型之间的实际程序调用的时候的顺序是根据对应的 mapping 的顺序进行调用的

web.xml文件详解 

web.xml首先是肯定要包含它的schema。

<web-app xmlns="http://java.sun.com/xml/ns/j2ee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee/web-app_2_5.xsd"
    version="2.5">

</web-app>

其它的元素都放在<web-app></web-app>之中。

<discription></discription> 是对站台的描述
<display-name></display-name> 定义站台的名称
<distributable/> 是指定该站台是否可分布式处理

context-param

<context-param></context-param> 用来设定web站台的环境参数,它包含两个子元素:

  • <param-name></param-name> 用来指定参数的名称
  • <param-value></param-value> 用来设定参数值

比如:

<context-param>
    <param-name>my_param</param-name>
    <param-value>hello</param-value>
</context-param>

在此设定的参数,可以在servlet中用 getServletContext().getInitParameter("my_param") 来取得。

filter

<filter></filter> 是用来声明filter的相关设定,它包含以下子元素:

  • <filter-name></filter-name> 指定filter的名字
  • <filter-class></filter-class> 定义filter的类的名称
  • <init-param></init-param> 用来定义参数,它有两个子元素:
    • <param-name></param-name> 用来指定参数的名称
    • <param-value></param-value> 用来设定参数值

比如:

<filter>
    <filter-name>setCharacterEncoding</filter-name>
    <filter-class>com.myTest.setCharacterEncodingFilter</filter-class>
    <init-param>
        <param-name>encoding</param-name>
        <param-value>GB2312</param-value>
    </init-param>
</filter>

与<filter></filter>同时使用的是<filter-mapping></filter-mapping> 用来定义filter所对应的URL,它有两个子元素:

  • <filter-name></filter-name> 指定filter的名字
  • <url-pattern></url-pattern> 指定filter所对应的URL

比如:

<filter-mapping>
    <filter-name>setCharacterEncoding</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

listener

<listener></listener> 用来设定Listener接口,它的主要子元素为:

  • <listener-class></listener-class> 定义Listener的类名称

比如:

<listener>
    <listener-class>com.myTest.ContextListener</listener-class> 
</listener>

servlet

<servlet></servlet> 用来声明一个servlet的数据,主要有以下子元素:

  • <servlet-name></servlet-name> 指定servlet的名称
  • <servlet-class></servlet-class> 指定servlet的类名称
  • <jsp-file></jsp-file> 指定web站台中的某个JSP网页的完整路径,等同于<servlet-class>一样,只是现在是要用jsp来替代servlet的功能。本质上jsp就是servlet。
  • <init-param></init-param> 用来定义参数,和前面的<init-param>差不多

同样,与<servlet></servlet>一起使用的是<servlet-mapping></servlet-mapping> 用来定义servlet所对应的URL,包含两个子元素:

  • <servlet-name></servlet-name> 指定servlet的名称
  • <url-pattern></url-pattern> 指定servlet所对应的URL

比如:

<servlet>
    <servlet-name>ShoppingServlet</servlet-name>
    <servlet-class>com.myTest.ShoppingServlet</servlet-class>
</servlet>

<servlet-mapping>
    <servlet-name>ShoppingServlet</servlet-name>
    <url-pattern>/shop/ShoppingServlet</url-pattern>
</servlet-mapping>

session-config

<session-config></session-config> 用来定义web站台中的session参数,包含一个子元素:

  • <session-timeout></session-timeout> 用来定义这个web站台所有session的有效期限,单位为分钟

mime-mapping

<mime-mapping></mime-mapping> 定义某一个扩展名和某一个MIME Type做对映,包含两个子元素:

  • <extension></extension> 扩展名的名称
  • <mime-type></mime-type> MIME格式

welcome-file-list

<welcome-file-list></welcom-file-list> 用来定义首页的列单,包含一个子元素:

  • <welcome-file></welcome-file> 指定首页的文件名称

比如:

<welcome-file-list>
    <welcome-file>index.jsp</welcome-file>
    <welcome-file>index.html</welcome-file>
</welcom-file-list>

error-page

<error-page></error-page> 用来处理错误代码或异常的页面,有三个子元素:

  • <error-code></error-code> 指定错误代码
  • <exception-type></exception-type> 指定一个JAVA异常类型
  • <location></location> 指定在web站台内的相关资源路径

比如:

<error-page>
    <error-code>404</error-code>
    <location>/error404.jsp</location>
</error-page>
<error-page>
    <exception-type>java.lang.Exception</exception-type>
    <location>/exception.jsp</location>
</error-page>

taglib

<taglib></taglib> 用来设定JSP网页所用到的Tag Library路径,有两个子元素:

  • <taglib-uri></taglib-uri> 定义TLD文件的URI,在JSP网页中用taglib指令便可取得该URI的TLD文件
  • <taglib-location></taglib-location> 指定TLD文件相对于web站台的存放位置

比如:

<taglib>
  <taglib-uri>myTaglib</taglib-uri>
  <taglib-location>/WEB-INF/tlds/MyTaglib.tld</taglib-location>
</taglib>

resource-ref

<resource-ref></resource-ref> 定义利用JNDI取得站台可利用的资源,有五个子元素:

  • <description></description> 资源说明
  • <rec-ref-name></rec-ref-name> 资源名称
  • <res-type></res-type> 资源种类
  • <res-auth></res-auth> 资源经由Application或Container来许可
  • <res-sharing-scope></res-sharing-scope> 资源是否可以共享,有Shareable和Unshareable两个值,默认为Shareable

比如,配置数据库连接池就可在此配置:

<resource-ref>
        <description>JNDI JDBC DataSource of shop</description>
        <res-ref-name>jdbc/sample_db</res-ref-name>
        <res-type>javax.sql.DataSource</res-type>
        <res-auth>Container</res-auth>
</resource-ref>

jsp-config

<jsp-config> 包括<taglib> 和<jsp-property-group> 两个子元素。

其中<taglib>元素在JSP 1.2时就已经存在;而<jsp-property-group>是JSP 2.0 新增的元素。

<jsp-property-group>元素主要有八个子元素,它们分别为:

  • <description>:设定的说明;
  • <display-name>:设定名称;
  • <url-pattern>:设定值所影响的范围,如:/CH2 或 /*.jsp;
  • <el-ignored>:若为true,表示不支持EL 语法;
  • <scripting-invalid>:若为true,表示不支持<% scripting %>语法;
  • <page-encoding>:设定JSP 网页的编码;
  • <include-prelude>:设置JSP 网页的抬头,扩展名为.jspf;
  • <include-coda>:设置JSP 网页的结尾,扩展名为.jspf。

 一个简单的<jsp-config>元素完整配置:

<jsp-config>
    <taglib>
        <taglib-uri>Taglib</taglib-uri>
        <taglib-location>/WEB-INF/tlds/MyTaglib.tld</taglib-location>
    </taglib>
    <jsp-property-group>
        <description>Special property group for JSP Configuration JSP example.</description>
        <display-name>JSPConfiguration</display-name>
        <url-pattern>/jsp/* </url-pattern>
        <el-ignored>true</el-ignored>
        <page-encoding>GB2312</page-encoding>
        <scripting-invalid>true</scripting-invalid>
        <include-prelude>/include/prelude.jspf</include-prelude>
        <include-coda>/include/coda.jspf</include-coda>
    </jsp-property-group>
</jsp-config>

security-constraint

有时我们只希望通过认证的用户才能请求某些servlet的话,就可以在web.xml中来进行相应的配置,来达到此目的。

这就要用到<security-constraint></security-constraint>元素。

对于tomcat,web.xml使用security-constraint元素需要在位于<Tomcat-installation-directory>/conf/tomcat-users.xml的XML文件中创建用户名和密码。比如下面的这个tomcat-users.xml文件:

<?xml version='1.0' encoding='utf-8'?>
<tomcat-users>
  <role rolename="tomcat"/>
  <role rolename="manager"/>
  <role rolename="admin"/>
  <user username="tomcat" password="tomcat" roles="tomcat"/>
  <user username="both" password="tomcat" roles="tomcat,manager"/>
  <user username="admin" password="admin" roles="admin"/>
</tomcat-users>

此XML片段包括一个tomcat-users根元素,它包含一个或多个role和user元素。

然后在Web应用程序的web.xml中创建security-constraint、login-config和security-role元素。

<security-constraint>
      <web-resource-collection>
          <web-resource-name>HelloServlet</web-resource-name>
          <url-pattern>/HelloServlet</url-pattern>
          <http-method>GET</http-method>
          <http-method>POST</http-method>
      </web-resource-collection>
      <auth-constraint>
          <description>This applies only to the "tomcat" security role</description>
          <role-name>admin</role-name>
      </auth-constraint>
      <user-data-constraint>
          <transport-guarantee>NONE</transport-guarantee>
      </user-data-constraint>
  </security-constraint>
  
  <login-config>
      <auth-method>BASIC</auth-method>
  </login-config>
  <security-role>
      <role-name>admin</role-name>
  </security-role>

其中security-constraint元素包含一个或多个web-resource-collection元素,它是描述Web应用程序中的哪些web资源受到指定安全限制的保护。http-method元素指定安全限制覆盖的HTTP方法。上面的例子中,当我们对/HelloServlet的GET或POST请求时将触发配置的安全机制。

auth-constraint元素用于描述允许访问Web组件的安全角色。此例中安全角色的例子有tomcat、manager、admin。而只有当作为admin角色的用户才可以访问HelloServlet。

Web应用程序通过login-config元素来认证用户,并确认该用户是否为正确的角色。

longin-config包含的transport-guarantee子元素用来指定认证方法,BASIC是一种常见的Web认证方式,浏览器给用户提示一个对话框,要求输入用户名和密码,随后Tomcat将给出的用户名和密码与tomcat-users.xml中的用户名和密码进行比较,然后使用前面的security-constraint配置来确定用户是否可访问受保护的servlet。

(除BASIC外,还可以是FORM、CLIENT-CERT、DIGEST等)

其实这种认证方法实际上有两个步骤:

  • 检查提供的用户名和密码是否正确。
  • 判断用户是否映射到特定的安全角色。例如,用户可能提供了正确的用户名和密码,但没有映射到特定的安全角色,也将被禁止访问特定的Web资源。

 

posted @ 2022-07-01 10:06  残城碎梦  阅读(1270)  评论(0编辑  收藏  举报