Java Web开发中的过滤器(Filter)

1、Filter介绍

Filter技术是servlet 2.3新增加的功能。servlet2.3是sun公司于2000年10月发布的,它的开发者包括许多个人和公司团体,充分体现了sun公司所倡导的代码开放性原则。在众多参与者的共同努力下,servlet2.3比以往功能都强大了许多,而且性能也有了大幅提高。
通过Filter功能,用户可以改变一个request和修改一个response。Filter不是一个servlet,它不能产生一个response,但是,它能够在一个request到达servlet之前预处理request,也可以在response离开servlet时处理response。
这样,Filter可以对所有的发往Java服务器的请求(包括对html,js,图片等静态资源的请求)做拦截,从而实现一些特殊的功能,如URL级别的权限访问控制、过滤敏感词汇、压缩响应信息等。

2、Filter执行机制

Servlet API中提供了一个Filter接口,开发web应用时,如果编写的Java类实现了这个接口,则把这个java类称之为过滤器Filter。通过Filter类,可以实现用户在访问某个目标资源之前,对访问的请求和响应进行拦截。
Filter接口中有方法void doFilter(ServletRequest req,ServletResponse res,FilterChain chain)用来执行filter的工作。每一个filter从doFilter()方法中得到当前的request及response对象。在这个方法里,可以进行任何的针对request及response的操作,包括收集数据,包装数据等。filter调用chain.doFilter()方法将请求从该filter中放行。之后,有可能请求到了要访问的资源对象,也有可能把控制权交给了下一个filter。
当我们编写好Filter,并配置对哪个web资源进行拦截后,WEB服务器每次在调用web资源的service方法之前,都会先调用一下filter的doFilter方法,因此,在该方法内编写代码可达到如下目的:

  • 调用目标资源之前,让一段代码执行。
  • 是否调用目标资源(即是否让用户访问web资源)。
  • 调用目标资源之后,让一段代码执行。

web服务器在调用doFilter方法时,会传递一个filterChain对象进来,filterChain对象是filter接口中最重要的一个对象,它也提供了一个doFilter方法,开发人员可以根据需求决定是否调用此方法,调用该方法,则web服务器就会调用web资源的service方法,即web资源就会被访问,否则web资源不会被访问。

3、Filter入门示意

这里我们编写一个简单的Filter示意代码。
首先,新建一个Filter类,实现javax.servlet.Filter接口。
com.lfqy.web.filter.FilterTrial01

package com.lfqy.web.filter;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

/**
 * Created by chengxia on 2019/11/4.
 * filter的三种典型应用:
 1、可以在filter中根据条件决定是否调用chain.doFilter(request, response)方法,即是否让目标资源执行
 2、在让目标资源执行之前,可以对request\response作预处理,再让目标资源执行
 3、在目标资源执行之后,可以捕获目标资源的执行结果,从而实现一些特殊的功能
 */
public class FilterTrial01 implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("The filter is initializing...");
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        //设置request和response的编码格式
        servletRequest.setCharacterEncoding("UTF-8");
        servletResponse.setCharacterEncoding("UTF-8");
        //设置请求的contenttype
        servletResponse.setContentType("text/html;charset=UTF-8");

        System.out.println("Before the request is handled...");
        System.out.println("url: " + ((HttpServletRequest)servletRequest).getRequestURL());
        //让目标资源执行,放行
        filterChain.doFilter(servletRequest,servletResponse);
        System.out.println("After the request is handled...");
    }

    @Override
    public void destroy() {
        System.out.println("The filter is destroying...");
    }
}

在这个filter的实现类中,在请求的执行前后,分别输出提示信息和请求的url到控制台。
然后,在配置文件中,添加这个filter的定义。
WEB-INF/web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
         version="3.1">
    <filter>
        <filter-name>FilterTrial01</filter-name>
        <filter-class>com.lfqy.web.filter.FilterTrial01</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>FilterTrial01</filter-name>
        <!-- "/*"表示拦截所有的请求 -->
        <url-pattern>*.jsp</url-pattern>
        <url-pattern>*.html</url-pattern>
    </filter-mapping>
</web-app>

从这个配置文件的内容,可以看出,filter的配置分为两部分:一部分是定义filter的名称和filter实现类的关联关系,另外一部分是定义filter所拦截请求的过滤规则。这里我们配置了两个请求url的拦截规则,也就是拦截所有对jsp和html页面的请求(前面已经说明filter可以拦截对静态资源的请求)。前一部分我们称为注册Filter,后一部分我们称为映射Filter。
这里需要注意urlpattern的配置规则:

  • /开头和以/*结尾的是用来做路径映射的。
  • 以前缀*.开头的是用来做扩展映射的。
  • /是用来定义default servlet映射的。
  • 剩下的都是用来定义详细映射的。比如: /aa/bb/cc.jsp

所以,如果urlpattern配置成/*.jsp会报错,因为它既属于路径映射,又属于后缀映射,应用服务器会困惑。

最后,编写两个测试页面。
index.jsp

<%--
  Created by IntelliJ IDEA.
  User: chengxia
  Date: 2019/11/4
  Time: 7:47 AM
  To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
  <head>
    <title>Trial Page for Filter</title>
  </head>
  <body>
  <h3>This is a filter page.</h3>
  </body>
</html>

test.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Gogogo</title>
</head>
<body>
<h1>gogogo</h1>
</body>
</html>

这样,启动服务器之后,分别访问http://localhost:8080/test.htmlhttp://localhost:8080/index.jsp,可以看到控制台输出如下:

Before the request is handled...
url: http://localhost:8080/test.html
After the request is handled...
Before the request is handled...
url: http://localhost:8080/index.jsp
After the request is handled...

4、Filter其它说明

4.1 Filter链

在一个web应用中,可以开发编写多个Filter,这些Filter组合起来称之为一个Filter链。
web服务器根据Filter在web.xml文件中的注册顺序,决定先调用哪个Filter,当第一个Filter的doFilter方法被调用时,web服务器会创建一个代表Filter链的FilterChain对象传递给该方法。在doFilter方法中,开发人员如果调用了FilterChain对象的doFilter方法,则web服务器会检查FilterChain对象中是否还有filter,如果有,则调用第2个filter,如果没有,则调用目标资源。

4.2 Filter生命周期

4.2.1 创建

Filter的创建和销毁由应用服务器负责。 web应用启动时,应用服务器将创建Filter的实例对象,并调用其init方法,完成对象的初始化功能,从而为后续的用户请求作好拦截的准备工作,filter对象只会创建一次,init方法也只会执行一次。通过init方法的参数,可获得代表当前filter配置信息的FilterConfig对象。

4.2.2 销毁

Web容器调用destroy方法销毁Filter。destroy方法在Filter的生命周期中仅执行一次。在destroy方法中,可以释放过滤器使用的资源。

4.2.3 FilterConfig接口

用户在配置filter时,可以使用<init-param>为filter配置一些初始化参数,当web容器实例化Filter对象,调用其init方法时,会把封装了filter初始化参数的filterConfig对象传递进来。因此开发人员在编写filter时,通过filterConfig对象的方法,就可获得:

  • String getFilterName():得到filter的名称。
  • String getInitParameter(String name): 返回在部署描述中指定名称的初始化参数的值。如果不存在返回null.
  • Enumeration getInitParameterNames():返回过滤器的所有初始化参数的名字的枚举集合。
  • public ServletContext getServletContext():返回Servlet上下文对象的引用。

如下是一个带参数的filter的例子。
com.lfqy.web.filter.FilterInitParamTrial

package com.lfqy.web.filter;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

/**
 * Created by chengxia on 2019/11/4.
 * filter的三种典型应用:
 1、可以在filter中根据条件决定是否调用chain.doFilter(request, response)方法,即是否让目标资源执行
 2、在让目标资源执行之前,可以对request\response作预处理,再让目标资源执行
 3、在目标资源执行之后,可以捕获目标资源的执行结果,从而实现一些特殊的功能
 */
public class FilterInitParamTrial implements Filter {
    private String filterName;
    private String filterFunc;
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("The filter is initializing...");
        filterName = filterConfig.getInitParameter("FilterName");
        filterFunc = filterConfig.getInitParameter("Function");
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        //设置request和response的编码格式
        servletRequest.setCharacterEncoding("UTF-8");
        servletResponse.setCharacterEncoding("UTF-8");
        //设置请求的contenttype
        servletResponse.setContentType("text/html;charset=UTF-8");

        System.out.println(filterName + " for function " + filterFunc + "executing. Before the request is handled...");
        System.out.println("url: " + ((HttpServletRequest)servletRequest).getRequestURL());
        //让目标资源执行,放行
        filterChain.doFilter(servletRequest,servletResponse);
        System.out.println(filterName + " for function " + filterFunc + "executing. After the request is handled...");
    }

    @Override
    public void destroy() {
        System.out.println("The filter is destroying...");
    }
}

WEB-INF/web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
         version="3.1">
    <filter>
        <filter-name>FilterTrial01</filter-name>
        <filter-class>com.lfqy.web.filter.FilterTrial01</filter-class>
    </filter>
    <filter>
        <filter-name>FilterInitParamTrial</filter-name>
        <filter-class>com.lfqy.web.filter.FilterInitParamTrial</filter-class>
        <init-param>
            <description>测试Filter配置参数01</description>
            <param-name>FilterName</param-name>
            <param-value>PaopaoFilter</param-value>
        </init-param>
        <init-param>
            <description>测试Filter配置参数02</description>
            <param-name>Function</param-name>
            <param-value>Test filter init param...</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>FilterTrial01</filter-name>
        <!-- "/*"表示拦截所有的请求 -->
        <url-pattern>*.jsp</url-pattern>
    </filter-mapping>
    <filter-mapping>
        <filter-name>FilterInitParamTrial</filter-name>
        <!-- "/*"表示拦截所有的请求 -->
        <url-pattern>*.html</url-pattern>
    </filter-mapping>
</web-app>

重启tomcat服务器,配置生效之后,访问http://localhost:8080/test.html,控制台输出如下:

PaopaoFilter for function Test filter init param...executing. Before the request is handled...
url: http://localhost:8080/test.html
PaopaoFilter for function Test filter init param...executing. After the request is handled...

可见,过滤器中配置的初始参数已经被拿到。

4.3 映射Filter时的dispatcher属性

<dispatcher>指定过滤器所拦截的资源被 Servlet 容器调用的方式,可以是REQUEST,INCLUDE,FORWARD和ERROR之一,默认REQUEST。用户可以设置多个<dispatcher> 子元素用来指定 Filter 对资源的多种调用方式进行拦截。如下:

<filter-name>filterName</filter-name>
   <url-pattern>/*</url-pattern>
   <dispatcher>REQUEST</dispatcher>
   <dispatcher>FORWARD</dispatcher>
</filter-mapping>

<dispatcher>子元素可以设置的值及其意义:

  • REQUEST:当用户直接访问页面时,Web容器将会调用过滤器。如果目标资源是通过RequestDispatcher的include()forward()方法访问时,那么该过滤器就不会被调用。
  • INCLUDE:如果目标资源是通过RequestDispatcher的include()方法访问时,那么该过滤器将被调用。除此之外,该过滤器不会被调用。
  • FORWARD:如果目标资源是通过RequestDispatcher的forward()方法访问时,那么该过滤器将被调用,除此之外,该过滤器不会被调用。
  • ERROR:如果目标资源是通过声明式异常处理机制调用时,那么该过滤器将被调用。除此之外,过滤器不会被调用。

5、Filter使用场景示例

5.1 装饰器模式

5.1.1 定义

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

5.1.2 使用场景说明

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

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

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

5.1.3 装饰器模式的实现

在实现装饰器模式时,首先看需要被增强对象继承了什么接口或父类,编写一个类也去继承这些接口或父类。然后,在类中定义一个变量,变量类型即需增强对象的类型,同时,在类中定义一个构造函数,构造函数的参数是需增强的对象。最后,覆盖需增强的方法,编写增强的代码。
这里需要注意,装饰器模式中,必须重新编写接口或者父类的所有方法。对于其中,根本不需要增强的方法,只需要简单调用被增强对象(装饰器类中的成员变量)的同名方法即可。

5.2 Servlet API中对于请求对象和响应对象的默认装饰器实现

Servlet API中提供了一个request对象的Decorator设计模式的默认实现类HttpServletRequestWrapper,HttpServletRequestWrapper类实现了request接口中的所有方法,但这些方法的内部实现都是仅仅调用了一下所包装的的 request对象的对应方法,以避免用户在对request对象进行增强时需要实现request接口中的所有方法。
Servlet API中也提供了response对象的Decorator设计模式的默认实现类HttpServletResponseWrapper,HttpServletResponseWrapper类实现了response接口中的所有方法,但这些方法的内部实现都是仅仅调用了一下所包装的的 response对象的对应方法,以避免用户在对response对象进行增强时需要实现response接口中的所有方法。
这两个现成的装饰器类中,都定义了各自的包装对象,实现了request对象和reponse对象对应接口的方法。只不过在方法的实现中,都是简单调用对应包装对象的方法。这样的好处是,当我们自己实现装饰器类的时候,只需要继承这两个线程的装饰器类,然后,重写我们想扩展功能的方法即可。

5.2.1 使用Decorator模式包装request对象解决get请求中的中文参数乱码问题

5.2.1.1 问题原因

Tomcat 7之前的版本,对于URL中字符的编码都是使用iso8859-1。对于Get请求来说,参数是放在URL中的,所以,对于GET请求,在获得请求参数的时候,如果没有进行合适的转码操作,取出的参数值就可能是乱码。Tomcat 8以后默认的URL编码格式是utf-8。
在Tomcat配置文件conf/server.xml中设置URIEncoding的值为UTF-8:

<Connector port="8080" protocol="HTTP/1.1"
           connectionTimeout="20000"
           redirectPort="8443"
           URIEncoding="utf-8"/>

但是不建议这么做。因为这样相当于应用层的代码依赖于Tomcat的设置,有损可移植性。
这里用的是Tomcat 8,为了说明这个例子,这里将Tomcat默认的URL编码方式改成了``

<Connector port="8080" protocol="HTTP/1.1"
           connectionTimeout="20000"
           redirectPort="8443"
           URIEncoding="iso-8859-1"/>

5.2.1.2 代码示例

首先,写一个解决编码问题的filter实现类,它的作用就是拦截请求,然后,将请求做一个转码包装,然后放行。
com.lfqy.web.filter.CharacterEncodingFilter

package com.lfqy.web.filter;

import com.lfqy.web.wrapper.CharacterEncodingRequest;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * Created by chengxia on 2019/11/13.
 */
public class CharacterEncodingFilter implements Filter {

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

    public void init(FilterConfig filterConfig){
        //在初始化时,得到过滤器的配置信息
        this.filterConfig = filterConfig;
    }
    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);

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

    public void destroy(){
        //do nothing in destory.
    }
}

写一个请求对象的包装类,在包装类中,重写获取请求参数的getParameter方法,如果是get请求,就转码之后,再返回值。
com.lfqy.web.wrapper.CharacterEncodingRequest

package com.lfqy.web.wrapper;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;

/**
 * Created by chengxia on 2019/11/11.
 */
public class CharacterEncodingRequest extends HttpServletRequestWrapper {
    //定义一个成员变量,该对象就是被增强对象
    private HttpServletRequest req;
    //定义一个构造函数,其参数是被增强对象
    public CharacterEncodingRequest(HttpServletRequest req){
        super(req);
        this.req = req;
    }

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

接下来写一个jsp页面,用来分别发送GET请求和POST请求:
EncodingFilterTest.jsp

<%--
  Created by IntelliJ IDEA.
  User: chengxia
  Date: 2019/11/13
  Time: 7:53 AM
  To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%--引入jstl标签库 --%>
<%@taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<html>
<head>
    <title>Encoding Filter Test</title>
</head>
<body>
<%--使用c:url标签构建url,构建好的url存储在reqGetUrl变量中--%>
<c:url value="/EncodingTestServlet" scope="page" var="reqGetUrl">
    <%--构建的url的附带的中文参数 ,参数名是:petname,值是:于泡泡--%>
    <c:param name="petname" value="于泡泡"></c:param>
</c:url>
<%--使用get的方式访问 --%>
<a href="${reqGetUrl}">超链接(get方式请求)</a>
<hr/>
<%--使用post方式提交表单 --%>
<form action="${pageContext.request.contextPath}/EncodingTestServlet" method="post">
    用户名:<input type="text" name="petname" value="于泡泡" />
    <input type="submit" value="post方式提交">
</form>

</body>
</html>

为了接收前面的POST请求,需要再写一个Servlet。
com.lfqy.web.servlet.EncodingTestServlet

package com.lfqy.web.servlet;

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

/**
 * Created by chengxia on 2019/11/13.
 */
public class EncodingTestServlet extends HttpServlet {
    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        //接收参数
        String petname = request.getParameter("petname");
        //获取请求方式
        String method = request.getMethod();
        //获取输出流
        PrintWriter out = response.getWriter();
        out.write("请求的方式:"+method);
        out.write("<br/>");
        out.write("接收到的参数:"+petname);
    }

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

最后看下配置文件WEB-INF/web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
         version="3.1">
    <filter>
        <filter-name>FilterTrial01</filter-name>
        <filter-class>com.lfqy.web.filter.FilterTrial01</filter-class>
    </filter>
    <filter>
        <filter-name>FilterInitParamTrial</filter-name>
        <filter-class>com.lfqy.web.filter.FilterInitParamTrial</filter-class>
        <init-param>
            <description>测试Filter配置参数01</description>
            <param-name>FilterName</param-name>
            <param-value>PaopaoFilter</param-value>
        </init-param>
        <init-param>
            <description>测试Filter配置参数02</description>
            <param-name>Function</param-name>
            <param-value>Test filter init param...</param-value>
        </init-param>
    </filter>
    <filter>
        <filter-name>CharacterEncodingFilter</filter-name>
        <filter-class>com.lfqy.web.filter.CharacterEncodingFilter</filter-class>
        <init-param>
            <description>设置编码信息</description>
            <param-name>charset</param-name>
            <param-value>utf-8</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>FilterTrial01</filter-name>
        <!-- "/*"表示拦截所有的请求 -->
        <url-pattern>*.jsp</url-pattern>
    </filter-mapping>
    <filter-mapping>
        <filter-name>FilterInitParamTrial</filter-name>
        <!-- "/*"表示拦截所有的请求 -->
        <url-pattern>*.html</url-pattern>
    </filter-mapping>
    <filter-mapping>
        <filter-name>CharacterEncodingFilter</filter-name>
        <!-- "/*"表示拦截所有的请求 -->
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    <servlet>
        <servlet-name>EncodingTestServlet</servlet-name>
        <servlet-class>com.lfqy.web.servlet.EncodingTestServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>EncodingTestServlet</servlet-name>
        <url-pattern>/EncodingTestServlet</url-pattern>
    </servlet-mapping>

    <servlet>
        <servlet-name>PostUrlParamTestServlet</servlet-name>
        <servlet-class>com.lfqy.web.servlet.PostUrlParamTestServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>PostUrlParamTestServlet</servlet-name>
        <url-pattern>/PostUrlParamTestServlet</url-pattern>
    </servlet-mapping>
</web-app>

启动项目在Tomcat服务器上运行,然后访问http://localhost:8080/EncodingFilterTest.jsp,效果如下:

测试页面

这样,无论点击链接还是点击按钮,都会能够得到正确的请求参数并回显。
GET请求

POST请求

5.2.2 使用Decorator模式包装request对象实现HTML字符的转义

用户在浏览器页面输入的内容,如果其中含有HTML的中需要转义的字符,需要进行转义之后才能够正常显示。这里就介绍如何通过装饰器实现该功能。
首先,新建一个装饰器类,用于实现请求内容中参数值的转义。
com.lfqy.web.wrapper.HTMLCharacterEscapeRequest

package com.lfqy.web.wrapper;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;

/**
 * Created by chengxia on 2019/11/11.
 */
public class HTMLCharacterEscapeRequest extends HttpServletRequestWrapper {
    //定义一个成员变量,该对象就是被增强对象
    private HttpServletRequest req;
    //定义一个构造函数,其参数是被增强对象
    public HTMLCharacterEscapeRequest(HttpServletRequest req){
        super(req);
        this.req = req;
    }

    @Override
    public String getParameter(String name){

        //获取参数的值
        String value= this.req.getParameter(name);
        if(value==null || value.length() ==0 ){
            return value;
        }
        return filter(value);
    }

    private String filter(String msg){
        if (msg == null){
            return null;
        }
        char content[] = new char[msg.length()];
        msg.getChars(0, msg.length(), content, 0);
        StringBuffer result = new StringBuffer(content.length + 50);
        for (int i = 0; i < content.length; i++) {
            switch (content[i]) {
                case '<':
                    result.append("&lt;");
                    break;
                case '>':
                    result.append("&gt;");
                    break;
                case '&':
                    result.append("&amp;");
                    break;
                case '"':
                    result.append("&quot;");
                    break;
                default:
                    result.append(content[I]);
            }
        }
        return result.toString();
    }
}

新建一个filter实现类。
com.lfqy.web.filter.HTMLCharacterEscapeFilter

package com.lfqy.web.filter;

import com.lfqy.web.wrapper.CharacterEncodingRequest;
import com.lfqy.web.wrapper.HTMLCharacterEscapeRequest;

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

/**
 * Created by chengxia on 2019/11/13.
 */
public class HTMLCharacterEscapeFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        //do nothing
    }

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

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

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

    public void destroy(){
        //do nothing in destory.
    }
}

写一个jsp页面,用来测试。
HTMLCharacterEscapeTest.jsp

<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<!DOCTYPE HTML>
<html>
<head>
    <title>Html Character Escape Test</title>
</head>

<body>
<form action="${pageContext.request.contextPath}/HTMLCharacterEscapeTestServlet" method="post">
    留言:
    <textarea rows="8" cols="70" name="message">
        <script type="text/javascript">
            alert("This is my house.")
        </script>
        <a href="http://www.poorage.com">An Unknown Website</a>
    </textarea>
    <input type="submit" value="发表">
</form>
</body>
</html>

写一个Servlet,用于接收测试页面发送的请求。
com.lfqy.web.servlet.HTMLCharacterEscapeTestServlet

package com.lfqy.web.servlet;

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

/**
 * Created by chengxia on 2019/11/13.
 */
public class HTMLCharacterEscapeTestServlet extends HttpServlet {
    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        //接收参数
        String msg = request.getParameter("message");
        //获取输出流
        PrintWriter out = response.getWriter();
        out.write("message got:"+"<br/>"+msg);
    }

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

最后,在配置文件中,配置Servlet和过滤器。
WEB-INF/web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
         version="3.1">
    <filter>
        <filter-name>FilterTrial01</filter-name>
        <filter-class>com.lfqy.web.filter.FilterTrial01</filter-class>
    </filter>
    <filter>
        <filter-name>FilterInitParamTrial</filter-name>
        <filter-class>com.lfqy.web.filter.FilterInitParamTrial</filter-class>
        <init-param>
            <description>测试Filter配置参数01</description>
            <param-name>FilterName</param-name>
            <param-value>PaopaoFilter</param-value>
        </init-param>
        <init-param>
            <description>测试Filter配置参数02</description>
            <param-name>Function</param-name>
            <param-value>Test filter init param...</param-value>
        </init-param>
    </filter>
    <filter>
        <filter-name>CharacterEncodingFilter</filter-name>
        <filter-class>com.lfqy.web.filter.CharacterEncodingFilter</filter-class>
        <init-param>
            <description>设置编码信息</description>
            <param-name>charset</param-name>
            <param-value>utf-8</param-value>
        </init-param>
    </filter>
    <filter>
        <filter-name>HTMLCharacterEscapeFilter</filter-name>
        <filter-class>com.lfqy.web.filter.HTMLCharacterEscapeFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>FilterTrial01</filter-name>
        <!-- "/*"表示拦截所有的请求 -->
        <url-pattern>*.jsp</url-pattern>
    </filter-mapping>
    <filter-mapping>
        <filter-name>FilterInitParamTrial</filter-name>
        <!-- "/*"表示拦截所有的请求 -->
        <url-pattern>*.html</url-pattern>
    </filter-mapping>
    <filter-mapping>
        <filter-name>CharacterEncodingFilter</filter-name>
        <!-- "/*"表示拦截所有的请求 -->
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    <filter-mapping>
        <filter-name>HTMLCharacterEscapeFilter</filter-name>
        <!-- "/*"表示拦截所有的请求 -->
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    <servlet>
        <servlet-name>EncodingTestServlet</servlet-name>
        <servlet-class>com.lfqy.web.servlet.EncodingTestServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>EncodingTestServlet</servlet-name>
        <url-pattern>/EncodingTestServlet</url-pattern>
    </servlet-mapping>

    <servlet>
        <servlet-name>PostUrlParamTestServlet</servlet-name>
        <servlet-class>com.lfqy.web.servlet.PostUrlParamTestServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>PostUrlParamTestServlet</servlet-name>
        <url-pattern>/PostUrlParamTestServlet</url-pattern>
    </servlet-mapping>

    <servlet>
        <servlet-name>HTMLCharacterEscapeTestServlet</servlet-name>
        <servlet-class>com.lfqy.web.servlet.HTMLCharacterEscapeTestServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>HTMLCharacterEscapeTestServlet</servlet-name>
        <url-pattern>/HTMLCharacterEscapeTestServlet</url-pattern>
    </servlet-mapping>
</web-app>

启动服务器之后,访问http://localhost:8080/HTMLCharacterEscapeTest.jsp,效果如下:

留言测试页面

点击页面上的发表,效果如下。
留言测试结果页面

可以看到,前面的留言内容中的字符都已经被转义了。

5.2.3 使用Decorator模式包装request对象实现敏感字符过滤

首先,新建一个filter实现类,在初始化方法中,读取敏感词列表,加载到内存。同时,实现一个内部类,包装request类,重新实现获取参数值的方法。
com.lfqy.web.filter.DirtyWordsFilter

package com.lfqy.web.filter;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.util.ArrayList;
import java.util.List;

/**
 * Created by chengxia on 2019/11/13.
 */
public class DirtyWordsFilter implements Filter {

    private FilterConfig config = null;
    private List<String> dirtyWords;

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

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

        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) resp;
        DirtyWordsRequest dirtyRequest = new DirtyWordsRequest(request);

        chain.doFilter(dirtyRequest, response);
    }

    @Override
    public void destroy() {

    }

    /**
     * @Method: getDirtyWords
     * @Description: 获取敏感字符
     * @Anthor:SpaceCatt
     *
     * @return
     */
    private List<String> getDirtyWords(){
        List<String> dirtyWords = new ArrayList<String>();
        String dirtyWordPath = config.getInitParameter("DirtyWordsFilePath");
        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: DirtyWordsRequest
     * @Description: 使用Decorator模式包装request对象,实现敏感字符过滤功能
     * @author: spacecat
     * @date: 2019-12-6 上午08:56:35
     *
     */
    class DirtyWordsRequest extends HttpServletRequestWrapper {
        private HttpServletRequest request;
        public DirtyWordsRequest(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;
        }
    }
}

然后,写一个测试页面,提交带敏感词的表单内容。
DirtyWordsFilterTest.jsp

<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<!DOCTYPE HTML>
<html>
<head>
    <title>Html Character Escape Test</title>
</head>

<body>
<form action="${pageContext.request.contextPath}/HTMLCharacterEscapeTestServlet" method="post">
    留言:
    <textarea rows="8" cols="70" name="message">
        这是我的留言,包含敏感词1,敏感词2等敏感词。哈哈
    </textarea>
    <input type="submit" value="发表">
</form>
</body>
</html>

这里表单的数据直接提交到com.lfqy.web.servlet.HTMLCharacterEscapeTestServlet即可,无需新写。
最后修改配置文件。
WEB-INF/web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
         version="3.1">
    <filter>
        <filter-name>FilterTrial01</filter-name>
        <filter-class>com.lfqy.web.filter.FilterTrial01</filter-class>
    </filter>
    <filter>
        <filter-name>FilterInitParamTrial</filter-name>
        <filter-class>com.lfqy.web.filter.FilterInitParamTrial</filter-class>
        <init-param>
            <description>测试Filter配置参数01</description>
            <param-name>FilterName</param-name>
            <param-value>PaopaoFilter</param-value>
        </init-param>
        <init-param>
            <description>测试Filter配置参数02</description>
            <param-name>Function</param-name>
            <param-value>Test filter init param...</param-value>
        </init-param>
    </filter>
    <filter>
        <filter-name>CharacterEncodingFilter</filter-name>
        <filter-class>com.lfqy.web.filter.CharacterEncodingFilter</filter-class>
        <init-param>
            <description>设置编码信息</description>
            <param-name>charset</param-name>
            <param-value>utf-8</param-value>
        </init-param>
    </filter>
    <filter>
        <filter-name>DirtyWordsFilter</filter-name>
        <filter-class>com.lfqy.web.filter.DirtyWordsFilter</filter-class>
        <init-param>
            <description>设置包含敏感词的配置文件路径</description>
            <param-name>DirtyWordsFilePath</param-name>
            <param-value>/WEB-INF/DirtyWords.txt</param-value>
        </init-param>
    </filter>
    <filter>
        <filter-name>HTMLCharacterEscapeFilter</filter-name>
        <filter-class>com.lfqy.web.filter.HTMLCharacterEscapeFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>FilterTrial01</filter-name>
        <!-- "/*"表示拦截所有的请求 -->
        <url-pattern>*.jsp</url-pattern>
    </filter-mapping>
    <filter-mapping>
        <filter-name>FilterInitParamTrial</filter-name>
        <!-- "/*"表示拦截所有的请求 -->
        <url-pattern>*.html</url-pattern>
    </filter-mapping>
    <filter-mapping>
        <filter-name>CharacterEncodingFilter</filter-name>
        <!-- "/*"表示拦截所有的请求 -->
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    <filter-mapping>
        <filter-name>HTMLCharacterEscapeFilter</filter-name>
        <!-- "/*"表示拦截所有的请求 -->
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    <filter-mapping>
        <filter-name>DirtyWordsFilter</filter-name>
        <!-- "/*"表示拦截所有的请求 -->
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    <servlet>
        <servlet-name>EncodingTestServlet</servlet-name>
        <servlet-class>com.lfqy.web.servlet.EncodingTestServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>EncodingTestServlet</servlet-name>
        <url-pattern>/EncodingTestServlet</url-pattern>
    </servlet-mapping>

    <servlet>
        <servlet-name>PostUrlParamTestServlet</servlet-name>
        <servlet-class>com.lfqy.web.servlet.PostUrlParamTestServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>PostUrlParamTestServlet</servlet-name>
        <url-pattern>/PostUrlParamTestServlet</url-pattern>
    </servlet-mapping>

    <servlet>
        <servlet-name>HTMLCharacterEscapeTestServlet</servlet-name>
        <servlet-class>com.lfqy.web.servlet.HTMLCharacterEscapeTestServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>HTMLCharacterEscapeTestServlet</servlet-name>
        <url-pattern>/HTMLCharacterEscapeTestServlet</url-pattern>
    </servlet-mapping>
</web-app>

启动tomcat之后,访问http://localhost:8080/DirtyWordsFilterTest.jsp,效果如下:

留言敏感词测试页面

点击发表,可以看到关键词已经都被替换:
留言敏感词测试结果页面

在实际中,我们经常将上面的三个过滤器合并成一个。如下:
com.lfqy.web.filter.TripleInOneFilter

package com.lfqy.web.filter;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.util.ArrayList;
import java.util.List;

/**
 * Created by chengxia on 2019/11/13.
 */
public class TripleInOneFilter implements Filter {

    private FilterConfig config = null;
    private List<String> dirtyWords;
    //设置默认的字符编码
    private String defaultCharset = "UTF-8";

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

    @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 = config.getInitParameter("charset");
        if(charset==null){
            charset = defaultCharset;
        }
        request.setCharacterEncoding(charset);
        response.setCharacterEncoding(charset);
        response.setContentType("text/html;charset="+charset);

        TripleInOneRequest filteredRequest = new TripleInOneRequest(request);

        chain.doFilter(filteredRequest, response);
    }

    @Override
    public void destroy() {

    }

    /**
     * @Method: getDirtyWords
     * @Description: 获取敏感字符
     * @Anthor:SpaceCat
     *
     * @return
     */
    private List<String> getDirtyWords(){
        List<String> dirtyWords = new ArrayList<String>();
        String dirtyWordPath = config.getInitParameter("DirtyWordsFilePath");
        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: TripleInOneRequest
     * @Description: 使用Decorator模式包装request对象,实现敏感字符过滤功能
     * @author: spacecat
     * @date: 2019-12-6 上午08:56:35
     *
     */
    class TripleInOneRequest extends HttpServletRequestWrapper {
        private HttpServletRequest request;
        public TripleInOneRequest(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;
            }

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

            //html字符转义
            value = filter(value);

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

        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("&lt;");
                        break;
                    case '>':
                        result.append("&gt;");
                        break;
                    case '&':
                        result.append("&amp;");
                        break;
                    case '"':
                        result.append("&quot;");
                        break;
                    default:
                        result.append(content[I]);
                }
            }
            return (result.toString());
        }
    }
}

在配置文件中添加的配置信息如下:
WEB-INF/web.xml

<filter>
    <filter-name>DirtyWordsFilter</filter-name>
    <filter-class>com.lfqy.web.filter.DirtyWordsFilter</filter-class>
    <init-param>
        <description>设置编码信息</description>
        <param-name>charset</param-name>
        <param-value>utf-8</param-value>
    </init-param>
    <init-param>
        <description>设置包含敏感词的配置文件路径</description>
        <param-name>DirtyWordsFilePath</param-name>
        <param-value>/WEB-INF/DirtyWords.txt</param-value>
    </init-param>
</filter>
<filter-mapping>
    <filter-name>TripleInOneFilter</filter-name>
    <!-- "/*"表示拦截所有的请求 -->
    <url-pattern>/*</url-pattern>
</filter-mapping>

5.2.4 使用Decorator设计模式包装response对象实现压缩响应正文内容

通过filter向目标页面传递一个自定义的response对象。在自定义的response对象中,重写getOutputStream方法和getWriter方法,使目标资源调用此方法输出页面内容时,获得的是我们自定义的ServletOutputStream对象。在我们自定义的ServletOuputStream对象中,重写write方法,使写出的数据写出到一个buffer中(这里实际上是写到了java.io.ByteArrayOutputStream中,这个流自带一个自动增长的buffer,所有到输出到该流的数据都会先到buffer)。当页面完成输出后,通过调用自定义response对象的getBuffer()方法,在filter中就可得到页面写出的数据,从而我们可以调用GzipOuputStream对数据进行压缩后再写出给浏览器,以此完成响应正文件压缩功能。
首先,实现一个自定义的过滤器类。
com.lfqy.web.filter.GzipFilter

package com.lfqy.web.filter;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import java.io.*;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.GZIPOutputStream;

/**
 * Created by chengxia on 2019/11/13.
 */
public class GzipFilter implements Filter {

    @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;
        BufferedResponse bufResp = new BufferedResponse(response);
        chain.doFilter(request, bufResp);
        //拿出缓存中的数据,压缩后再打给浏览器
        byte out[] = bufResp.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);
    }

    @Override
    public void destroy() {

    }

    /**
     * @ClassName: BufferedResponse
     * @Description:
     * 使用Decorator模式包装response对象,输出到带缓存的ByteArrayOutputStream功能
     * 同时,实现了getBuffer方法,使能够拿到缓冲区的内容
     * @author: spacecat
     * @date: 2019-12-6 上午08:56:35
     *
     */
    class BufferedResponse extends HttpServletResponseWrapper {

        private ByteArrayOutputStream bout = new ByteArrayOutputStream();
        private PrintWriter pw;
        private HttpServletResponse response;
        public BufferedResponse(HttpServletResponse response) {
            super(response);
            this.response = response;
        }
        /**
         * 这个方法是我们压缩之后,再向浏览器写数据的时候,要调用的。
         * */
        @Override
        public ServletOutputStream getOutputStream() throws IOException {
            return new MyServletOutputStream(bout);
        }
        /**
         * 这个方法是框架输出到response对象时调用的
         * */
        @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);
        }

        @Override
        public boolean isReady() {
            return false;
        }

        @Override
        public void setWriteListener(WriteListener writeListener) {
        }
    }
}

修改配置文件,说明那些需要压缩。
WEB-INF/web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
         version="3.1">
    <filter>
        <filter-name>FilterTrial01</filter-name>
        <filter-class>com.lfqy.web.filter.FilterTrial01</filter-class>
    </filter>
    <filter>
        <filter-name>FilterInitParamTrial</filter-name>
        <filter-class>com.lfqy.web.filter.FilterInitParamTrial</filter-class>
        <init-param>
            <description>测试Filter配置参数01</description>
            <param-name>FilterName</param-name>
            <param-value>PaopaoFilter</param-value>
        </init-param>
        <init-param>
            <description>测试Filter配置参数02</description>
            <param-name>Function</param-name>
            <param-value>Test filter init param...</param-value>
        </init-param>
    </filter>
    <filter>
        <filter-name>CharacterEncodingFilter</filter-name>
        <filter-class>com.lfqy.web.filter.CharacterEncodingFilter</filter-class>
        <init-param>
            <description>设置编码信息</description>
            <param-name>charset</param-name>
            <param-value>utf-8</param-value>
        </init-param>
    </filter>
    <filter>
        <filter-name>DirtyWordsFilter</filter-name>
        <filter-class>com.lfqy.web.filter.DirtyWordsFilter</filter-class>
        <init-param>
            <description>设置编码信息</description>
            <param-name>charset</param-name>
            <param-value>utf-8</param-value>
        </init-param>
        <init-param>
            <description>设置包含敏感词的配置文件路径</description>
            <param-name>DirtyWordsFilePath</param-name>
            <param-value>/WEB-INF/DirtyWords.txt</param-value>
        </init-param>
    </filter>
    <filter>
        <filter-name>TripleInOneFilter</filter-name>
        <filter-class>com.lfqy.web.filter.TripleInOneFilter</filter-class>
        <init-param>
            <description>设置包含敏感词的配置文件路径</description>
            <param-name>DirtyWordsFilePath</param-name>
            <param-value>/WEB-INF/DirtyWords.txt</param-value>
        </init-param>
    </filter>
    <filter>
        <filter-name>GzipFilter</filter-name>
        <filter-class>com.lfqy.web.filter.GzipFilter</filter-class>
    </filter>
    <filter>
        <filter-name>HTMLCharacterEscapeFilter</filter-name>
        <filter-class>com.lfqy.web.filter.HTMLCharacterEscapeFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>FilterTrial01</filter-name>
        <!-- "/*"表示拦截所有的请求 -->
        <url-pattern>*.jsp</url-pattern>
    </filter-mapping>
    <filter-mapping>
        <filter-name>FilterInitParamTrial</filter-name>
        <!-- "/*"表示拦截所有的请求 -->
        <url-pattern>*.html</url-pattern>
    </filter-mapping>
    <filter-mapping>
        <filter-name>CharacterEncodingFilter</filter-name>
        <!-- "/*"表示拦截所有的请求 -->
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    <filter-mapping>
        <filter-name>HTMLCharacterEscapeFilter</filter-name>
        <!-- "/*"表示拦截所有的请求 -->
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    <filter-mapping>
        <filter-name>DirtyWordsFilter</filter-name>
        <!-- "/*"表示拦截所有的请求 -->
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    <filter-mapping>
        <filter-name>TripleInOneFilter</filter-name>
        <!-- "/*"表示拦截所有的请求 -->
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    <!--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>

    <servlet>
        <servlet-name>EncodingTestServlet</servlet-name>
        <servlet-class>com.lfqy.web.servlet.EncodingTestServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>EncodingTestServlet</servlet-name>
        <url-pattern>/EncodingTestServlet</url-pattern>
    </servlet-mapping>

    <servlet>
        <servlet-name>PostUrlParamTestServlet</servlet-name>
        <servlet-class>com.lfqy.web.servlet.PostUrlParamTestServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>PostUrlParamTestServlet</servlet-name>
        <url-pattern>/PostUrlParamTestServlet</url-pattern>
    </servlet-mapping>

    <servlet>
        <servlet-name>HTMLCharacterEscapeTestServlet</servlet-name>
        <servlet-class>com.lfqy.web.servlet.HTMLCharacterEscapeTestServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>HTMLCharacterEscapeTestServlet</servlet-name>
        <url-pattern>/HTMLCharacterEscapeTestServlet</url-pattern>
    </servlet-mapping>
</web-app>

这样,启动tomcat服务器之后,访问http://localhost:8080/DirtyWordsFilterTest.jsp,页面正常显示,服务器控制台输出如下:

Before the request is handled...
url: http://localhost:8080/DirtyWordsFilterTest.jsp
原始大小:376
压缩后的大小:300
After the request is handled...

5.2.5 使用Decorator设计模式包装response对象实现缓存数据到内存

对于页面中很少更新的数据,例如商品分类,为避免每次都要从数据库查询分类数据,因此可把分类数据缓存在内存或文件中,以此来减轻数据库或者磁盘的压力,提高系统响应速度。
原理上,这里也是用自定义的Response对象将输出先放到缓存,然后,缓存的数据放到一个映射结构中。这样,后续如果在内存中命中,就不用重新查询数据库或者访问磁盘了。
首先写一个自定义过滤器实现类。
com.lfqy.web.filter.WebResourceCachedFilter

package com.lfqy.web.filter;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
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 java.util.zip.GZIPOutputStream;

/**
 * Created by chengxia on 2019/11/13.
 */
public class WebResourceCachedFilter implements Filter {

    //用一个map结构缓存web资源
    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.如果缓存没有,让目标资源执行,并捕获目标资源的输出
        BufferedResponse myresponse = new BufferedResponse(response);
        chain.doFilter(request, myresponse);
        //获取缓冲流中的内容的字节数组
        byte out[] = myresponse.getBuffer();
        //5.把资源的数据以用户请求的uri为关键字保存到缓存中
        map.put(uri, out);
        //6.把数据打给浏览器
        response.getOutputStream().write(out);
    }

    @Override
    public void destroy() {

    }

    /**
     * @ClassName: BufferedResponse
     * @Description:
     * 使用Decorator模式包装response对象,输出到带缓存的ByteArrayOutputStream功能
     * 同时,实现了getBuffer方法,使能够拿到缓冲区的内容
     * @author: spacecat
     * @date: 2019-12-6 上午08:56:35
     *
     */
    class BufferedResponse extends HttpServletResponseWrapper {

        private ByteArrayOutputStream bout = new ByteArrayOutputStream();
        private PrintWriter pw;
        private HttpServletResponse response;
        public BufferedResponse(HttpServletResponse response) {
            super(response);
            this.response = response;
        }
        /**
         * 这个方法是我们压缩之后,再向浏览器写数据的时候,要调用的。
         * */
        @Override
        public ServletOutputStream getOutputStream() throws IOException {
            return new MyServletOutputStream(bout);
        }
        /**
         * 这个方法是框架输出到response对象时调用的
         * */
        @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);
        }

        @Override
        public boolean isReady() {
            return false;
        }

        @Override
        public void setWriteListener(WriteListener writeListener) {
        }
    }
}

修改配置文件,指定缓存那些页面。
WEB-INF/web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
         version="3.1">
    <filter>
        <filter-name>FilterTrial01</filter-name>
        <filter-class>com.lfqy.web.filter.FilterTrial01</filter-class>
    </filter>
    <filter>
        <filter-name>FilterInitParamTrial</filter-name>
        <filter-class>com.lfqy.web.filter.FilterInitParamTrial</filter-class>
        <init-param>
            <description>测试Filter配置参数01</description>
            <param-name>FilterName</param-name>
            <param-value>PaopaoFilter</param-value>
        </init-param>
        <init-param>
            <description>测试Filter配置参数02</description>
            <param-name>Function</param-name>
            <param-value>Test filter init param...</param-value>
        </init-param>
    </filter>
    <filter>
        <filter-name>CharacterEncodingFilter</filter-name>
        <filter-class>com.lfqy.web.filter.CharacterEncodingFilter</filter-class>
        <init-param>
            <description>设置编码信息</description>
            <param-name>charset</param-name>
            <param-value>utf-8</param-value>
        </init-param>
    </filter>
    <filter>
        <filter-name>DirtyWordsFilter</filter-name>
        <filter-class>com.lfqy.web.filter.DirtyWordsFilter</filter-class>
        <init-param>
            <description>设置编码信息</description>
            <param-name>charset</param-name>
            <param-value>utf-8</param-value>
        </init-param>
        <init-param>
            <description>设置包含敏感词的配置文件路径</description>
            <param-name>DirtyWordsFilePath</param-name>
            <param-value>/WEB-INF/DirtyWords.txt</param-value>
        </init-param>
    </filter>
    <filter>
        <filter-name>TripleInOneFilter</filter-name>
        <filter-class>com.lfqy.web.filter.TripleInOneFilter</filter-class>
        <init-param>
            <description>设置包含敏感词的配置文件路径</description>
            <param-name>DirtyWordsFilePath</param-name>
            <param-value>/WEB-INF/DirtyWords.txt</param-value>
        </init-param>
    </filter>
    <filter>
        <filter-name>GzipFilter</filter-name>
        <filter-class>com.lfqy.web.filter.GzipFilter</filter-class>
    </filter>
    <filter>
        <filter-name>WebResourceCachedFilter</filter-name>
        <filter-class>com.lfqy.web.filter.WebResourceCachedFilter</filter-class>
    </filter>
    <filter>
        <filter-name>HTMLCharacterEscapeFilter</filter-name>
        <filter-class>com.lfqy.web.filter.HTMLCharacterEscapeFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>FilterTrial01</filter-name>
        <!-- "/*"表示拦截所有的请求 -->
        <url-pattern>*.jsp</url-pattern>
    </filter-mapping>
    <filter-mapping>
        <filter-name>FilterInitParamTrial</filter-name>
        <!-- "/*"表示拦截所有的请求 -->
        <url-pattern>*.html</url-pattern>
    </filter-mapping>
    <filter-mapping>
        <filter-name>CharacterEncodingFilter</filter-name>
        <!-- "/*"表示拦截所有的请求 -->
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    <filter-mapping>
        <filter-name>HTMLCharacterEscapeFilter</filter-name>
        <!-- "/*"表示拦截所有的请求 -->
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    <filter-mapping>
        <filter-name>DirtyWordsFilter</filter-name>
        <!-- "/*"表示拦截所有的请求 -->
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    <filter-mapping>
        <filter-name>TripleInOneFilter</filter-name>
        <!-- "/*"表示拦截所有的请求 -->
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    <!--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>
    
    <filter-mapping>
        <filter-name>WebResourceCachedFilter</filter-name>
        <!-- 映射需要缓存输出的JSP页面,这几个页面都只是单纯作为输入UI,不会有太多的变化,因此可以缓存输出 -->
        <url-pattern>/DirtyWordsFilterTest.jsp</url-pattern>
        <url-pattern>/test.jsp</url-pattern>
        <url-pattern>/test2.jsp</url-pattern>
    </filter-mapping>
    
    <servlet>
        <servlet-name>EncodingTestServlet</servlet-name>
        <servlet-class>com.lfqy.web.servlet.EncodingTestServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>EncodingTestServlet</servlet-name>
        <url-pattern>/EncodingTestServlet</url-pattern>
    </servlet-mapping>

    <servlet>
        <servlet-name>PostUrlParamTestServlet</servlet-name>
        <servlet-class>com.lfqy.web.servlet.PostUrlParamTestServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>PostUrlParamTestServlet</servlet-name>
        <url-pattern>/PostUrlParamTestServlet</url-pattern>
    </servlet-mapping>

    <servlet>
        <servlet-name>HTMLCharacterEscapeTestServlet</servlet-name>
        <servlet-class>com.lfqy.web.servlet.HTMLCharacterEscapeTestServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>HTMLCharacterEscapeTestServlet</servlet-name>
        <url-pattern>/HTMLCharacterEscapeTestServlet</url-pattern>
    </servlet-mapping>
</web-app>

启动服务器之后,第二次访问http://localhost:8080/DirtyWordsFilterTest.jsp,服务器控制台输出如下:

Before the request is handled...
url: http://localhost:8080/DirtyWordsFilterTest.jsp

<!DOCTYPE HTML>
<html>
<head>
    <title>Html Character Escape Test</title>
</head>

<body>
<form action="/HTMLCharacterEscapeTestServlet" method="post">
    留言:
    <textarea rows="8" cols="70" name="message">
        这是我的留言,包含敏感词1,敏感词2等敏感词。哈哈
    </textarea>
    <input type="submit" value="发表">
</form>
</body>
</html>
原始大小:376
压缩后的大小:300
After the request is handled...

注意,在最后的两个例子中,并没有调用chain.doFilter(filteredRequest, response);,也就是说,请求经过这两个filter处理之后,就不会继续往别的过滤器传递了。这里需要特别注意。

参考资料

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,688评论 1 330
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,559评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,749评论 0 226
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,581评论 0 191
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,741评论 3 271
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,684评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,122评论 2 292
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,847评论 0 182
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,441评论 0 228
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,939评论 2 232
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,333评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,783评论 2 236
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,275评论 3 220
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,830评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,444评论 0 180
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,553评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,618评论 2 249

推荐阅读更多精彩内容