Java Web 高级编程 - 第九章 使用过滤器改进应用程序

本章内容

  • 过滤器的目的
  • 创建、声明和映射过滤器
  • 对过滤器进行排序
  • 在异步请求处理中使用过滤器
  • 学习过滤器的实际应用
  • 使用过滤器简化认证

 

9.1 了解过滤器的目的

过滤器是可以拦截访问资源的请求、资源的响应或者同事拦截两者的应用组件,它们将以某种方式作用域这些请求或响应。过滤器可以检测和修改请求和响应,它们甚至可以拒绝、重定向或转发请求。

javax.servlet.Filter

1.日志过滤器

2.验证过滤器

3.压缩和加密过滤器

4.错误处理过滤器

 

9.2 创建、声明和映射过滤器

9.2.1 了解过滤器链

尽管只有一个Servlet可以处理请求,但可以使用许多过滤拦截请求。

下图演示了过滤器链如何接受进入的请求并将它传递到下一个过滤器,直至所有匹配的过滤器都处理完成,最终再将它传入Servlet中。

调用FilterChain.doFilter()将处罚过滤器链的继续执行。如果当前处理器是过滤器链的最后一个过滤器,那么调用FilterChain.doFilter()将把控制权返回到Servlet容器中,它将把请求传递给Servlet。如果当前的过滤器未调用FilterChain.doFilter(),那么过滤器链将被中断,Servlet和所有其他剩余的过滤器都无法再处理该请求。

9.9.2 映射到URL模式和Servlet名称

如同Servlet一样,过滤器可以被映射到URL模式。这会决定哪个或哪些过滤器将拦截某个请求。任何匹配某个过滤器的URL模式的请求在被匹配的Servlet处理之前将首先进入该过滤器。通过使用URL模式,我们不但可以拦截Servlet请求,还可以拦截其他资源,例如图片、CSS文件、JS文件等。

也可以将过滤器映射到一个或多个Servlet名称。如果请求匹配于某个Servlet,容器将寻找所有匹配该Servlet名称的过滤器,并将它们应用到请求上。

9.2.3 映射到不同的请求派发器类型

在Servlet容器中,可以通过多种方式派发请求:

  • 普通请求 - 这些请求来自于客户端,并包含了容器中特定Web应用程序的目标URL。
  • 转发请求 - 当代吗调用RequestDispatcherforward方法或者使用<jsp:forward>标签时将触发这些请求。尽管它们被关联到原始的请求,但在内部它们将被作为单独的请求进行处理。
  • 包含请求 - 类似地,使用<jsp:include>标签或者调用RequestDispatcherinclude方法时,将会产生一个不同的、与原始请求相关的内部包含请求。
  • 错误资源请求 - 这些是访问处理HTTP错误(例如 404 Not Found、500 Internal Server Error)的错误页面的请求。

在声明和映射过滤器时,需要指明派发器类型或者过滤器应该作用于的类型。

9.2.4 使用部署描述符

在编写的过滤器拦截请求之前,必须如同Servlet一样声明和映射它们。如同Servlet一样,可以通过多种方式实现该设置。传统的方式是在部署描述符中使用<filter>和<filter-mapping>元素(类似<servlet>和<servlet-mapping>)。<filter>必须至少包含一个名字和类名,它还可以包含描述、显示名称、图标以及一个或多个初始化参数。

与Servlet不同的是,过滤器不可以在第一个请求到达时加载。过滤器的init方法总是在应用程序启动时调用:在ServletContextListener初始化之后,Servlet初始化之前,它们将按照部署描述符中出现的顺序依次加载。

在声明了过滤器之后,可以将它映射到任意数目的URL或Servlet名称。如同Servlet URL映射一样,过滤器URL映射还可以包含通配符。

<filter-mapping> 
    <filter-name>myFilter</filter-name> 
    <url-pattern>/foo</url-pattern> 
    <url-pattern>/bar/*</url-pattern>
    <servlet-name>myServlet</servlet-name> 
    <dispatcher>REQUEST</dispatcher> 
    <dispatcher>ASYNC</dispatcher>
</filter-mapping>

上例中,过滤器将会响应所有相对于应用程序URL/foo和/bar/*的请求,以及任何最终由myServlet处理的请求。这里的两个<dispatcher>元素意味着它可以响应普通请求和有AsyncContext派发的请求。过滤器映射可以有0个或者多个<dispatcher>元素。如果未指定,那么默认将只能使用REQUEST派发器。

有效地DispatcherType包含:ASYNCERRORFORWARDINCLUDEREQUEST

9.2.5 使用注解

如同Servlet一样,可以使用注解声明和映射过滤器。注解WebFilter中包含了取代属性描述符所有选项的特性。

@WebFilter(
    filterName = "myFilter",
    urlPatterns = { "/foo", "/bar/*" },
    servletNames = { "myServlet" },
    dispatcherTypes = { DispatcherType.REQUEST, DispatcherType.ASYNC }
)
public class MyFilter implements Filter

使用注解声明和映射的主要缺点是:不能对过滤器链上的过滤器进行排序。为了使过滤器正确运行,特定的执行顺序是非常重要的。如果希望在不使用部署描述符的情况下,控制过滤器执行的顺序,那么就需要使用编程式配置。

9.2.6 使用编程式配置

如同Servlet、监听器和其他组件一样,可以在ServletContext中以编程的方式配置过滤器。不使用部署描述符和注解,调用ServletContext的方法注册和映射过滤器即可。

因为这需要在ServletContext结束启动之前完成,所以通常需要在ServletContextListener.contextInitialized方法中实现(也可以在ServletContainerInitializer.onStartup方法中添加过滤器。

@WebListener
public class Configurator implements ServletContextListener {
    @Override
    public void contextInitialized(ServletContextEvent event) {
        ServletContext context = event.getServletContext();
        
        FilterRegistration.Dynamic registration = context.addFilter("myFilter", new MyFilter());
        
        registration.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST, DispatcherType.ASYNC), false, "/foo", "/bar/*"); 

        registration.addMappingForServletNames(EnumSet.of(DispatcherType.REQUEST, DispatcherType.ASYNC), false, "myServlet"); 
    }
}

上例中使用addFilter方法将过滤器添加到了ServletContext中。它将返回一个FilterRegistration.Dynamic对象,可以使用该对象为URL模式和Servlet名称添加过滤器映射。

addMappingForUrlPatternsaddMappingForServletNames

void addMappingForServletNames(EnumSet<DispatcherType> dispatcherTypes,
                                boolean isMatchAfter,
                                String... servletNames)

void addMappingForUrlPatterns(EnumSet<DispatcherType> dispatcherTypes,
                                boolean isMatchAfter,
                                String... urlPatterns)

第一个参数:EnumSet<DispatcherType>。如同部署描述符一样,如果派发类型参数为null,那么它将使用默认的REQUEST派发器。

第二个参数表示该过滤器相对于部署描述符中过滤器的顺序。如果使用的参数为false,那么编程式的过滤器映射将在部署描述符的所有过滤器家在之前进行加载并排序;如果为true,那么部署描述符中的映射将先被加载。

最后一个参数指定了过滤器映射到的URL模式(addMappingForUrlPatterns)或者Servlet名称(addMappingForServletNames)。

 

9.3 过滤器排序

9.3.1 URL模式映射和Servlet名称映射

定义过滤器顺序是很简单的:匹配请求的过滤器将按照它们出现在部署描述符或者编程式配置中的顺序添加到过滤器链中(记住,如果同时在部署描述符或者编程式配置中设置了一些过滤器,那么需要在编程式配置中使用addMappingForUrlPatternsaddMappingForServletNames方法的第二个参数,决定编程式映射是否应该出现在XML映射之前)。

URL映射的过滤器优先级比Servlet名称映射的过滤器高。如果两个过滤器都可以匹配某个请求,一个是URL模式而另一个是Servlet名称,那么在过滤器链中,由URL模式匹配的过滤器总是出现在由Servlet名称匹配的过滤器之前。

<servlet-mapping> 
    <servlet-name>myServlet</servlet-name> 
    <url-pattern>/foo*</url-pattern>
</servlet-mapping>
<filter-mapping> 
    <filter-name>servletFilter</filter-name> 
    <servlet-name>myServlet</servlet-name>
</filter-mapping>
<filter-mapping> 
    <filter-name>myFilter</filter-name> 
    <url-pattern>/foo*</url-pattern>
</filter-mapping>
<filter-mapping> 
    <filter-name>anotherFilter</filter-name> 
    <url-pattern>/foo/bar</url-pattern>
</filter-mapping>

如果一个普通的请求访问的URL是/foo/bar,那么它将匹配所有这三个过滤器。过滤器链将由3各过滤器组成,依次为myFileter,anotherFileter,然后是servletFileter。myFilter将在anotherFilter之前执行,因为这是它们出现在部署描述符中的顺序。它们都将在servletFilter之前执行,因为URL映射总是在Servlet名称映射之前执行。如果这是一个转发、包含、错误派发或者一部派发的请求,它就不会匹配任何一个过滤器,因为这些映射并未显式地指定任何<dispatcher>元素。

9.3.2 演示过滤器顺序

Filter-Order项目。它包含了3个Servlet和3个过滤器。下面的代码脚本ServletOne,它与对应的ServletTwo和ServletThree一致,除了所有的One分别被替换成了Two和Three。

@WebServlet(name = "servletOne", urlPatterns = "/servletOne")
public class ServletOne extends HttpServlet
{
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException
    {
        System.out.println("Entering ServletOne.doGet().");
        response.getWriter().write("Servlet One");
        System.out.println("Leaving ServletOne.doGet().");
    }
}

 

类似的,下面的FilterA与FilterB和FilterC一致:

public class FilterA implements Filter
{
    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain) throws IOException, ServletException
    {
        System.out.println("Entering FilterA.doFilter().");
        chain.doFilter(request, response);
        System.out.println("Leaving FilterA.doFilter().");
    }

    @Override
    public void init(FilterConfig config) throws ServletException { }

    @Override
    public void destroy() { }
}

 

为了匹配之前图示中的过滤器映射,按照下面的代码对不熟描述符的过滤器映射进行设置。

    <filter>
        <filter-name>filterA</filter-name>
        <filter-class>com.wrox.FilterA</filter-class>
    </filter>

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

    <filter>
        <filter-name>filterB</filter-name>
        <filter-class>com.wrox.FilterB</filter-class>
    </filter>

    <filter-mapping>
        <filter-name>filterB</filter-name>
        <url-pattern>/servletTwo</url-pattern>
        <url-pattern>/servletThree</url-pattern>
    </filter-mapping>

    <filter>
        <filter-name>filterC</filter-name>
        <filter-class>com.wrox.FilterC</filter-class>
    </filter>

    <filter-mapping>
        <filter-name>filterC</filter-name>
        <url-pattern>/servletTwo</url-pattern>
    </filter-mapping>

 

(1)在浏览器中访问http://localhost:8080/filters/servletOne,IDE中的Tomcat的标准输出中将显示出几条消息:

Entering FilterA.doFilter().
Entering ServletOne.doGet().
Leaving ServletOne.doGet().
Leaving FilterA.doFilter().

(2)将浏览器地址修改为/servletTwo,标准输出中将出现新的消息:

Entering FilterA.doFilter().
Entering FilterB.doFilter().
Entering FilterC.doFilter().
Entering ServletTwo.doGet().
Leaving ServletTwo.doGet().
Leaving FilterC.doFilter().
Leaving FilterB.doFilter().
Leaving FilterA.doFilter().

现在过滤器链将从A执行到C,然后再执行Servlet。接着在Servlet完成请求处理后,过滤器链将按照相反的顺序从C执行到A。

(3)将浏览器中的地址修改为/servletThree。它的输出将如下所示:

Entering FilterA.doFilter().
Entering FilterB.doFilter().
Entering ServletThree.doGet().
Leaving ServletThree.doGet().
Leaving FilterB.doFilter().
Leaving FilterA.doFilter().

9.3.3 使用过滤器处理异步请求

异步请求处理的关键在于:Servlet的service方法可以在响应发送到客户端之前返回。然后请求处理将被委托到另一个线程或者基于某些事件完成。

例如,service方法(或者doGet、doPost及其他方法)可以启动AsyncContext,然后注册某种类型目标消息(例如收到一个聊天请求)的监听器,然后返回。接着当目标消息的监听器收到消息之后,他可以将响应返回到用户。通过使用这种技术,在请求处理暂停的时候,请求线程也不会被阻塞。拦截这样请求的过滤器将在响应真正发送之前完成,因为当service返回时,FilterChain的doChain方法也将返回。

映射到ASYNC派发器的过滤器将拦截调用AsyncContext的dispatch方法得到的内部请求。查看Filter-Async项目。它的AnyRequestFilter封装了请求和响应,并可以过滤任何类型的请求。如果它检测到Servlet启动了AsyncContext,那么它将输出该信息,并表示AsyncContext是否使用原始的请求和响应对象或者未封装的请求和响应对象。

public class AnyRequestFilter implements Filter
{
    private String name;

    @Override
    public void init(FilterConfig config)
    {
        this.name = config.getFilterName();
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain) throws IOException, ServletException
    {
        System.out.println("Entering " + this.name + ".doFilter().");
        chain.doFilter(
                new HttpServletRequestWrapper((HttpServletRequest)request),
                new HttpServletResponseWrapper((HttpServletResponse)response)
        );
        if(request.isAsyncSupported() && request.isAsyncStarted())
        {
            AsyncContext context = request.getAsyncContext();
            System.out.println("Leaving " + this.name + ".doFilter(), async " +
                    "context holds wrapped request/response = " +
                    !context.hasOriginalRequestAndResponse());
        }
        else
            System.out.println("Leaving " + this.name + ".doFilter().");
    }

    @Override
    public void destroy() { }
}

 

在web.xml中,该过滤器将被实例化和映射3次。所有这3个映射都可以拦截任何URL,但normalFilter实例只负责拦截普通请求;forwardFilter只拦截转发请求;asyncFilter只拦截由AsyncContext派发的请求。

每个<filter>元素中添加的<async-supported>true</async-supported>。将告诉容器该过滤器是用于处理异步请求的,如果一个未启用<async-supported>的过滤器过滤一个请求,并尝试在请求中启动AsyncContext,那么将会导致IllegalStateException异常。

    <filter>
        <filter-name>normalFilter</filter-name>
        <filter-class>com.wrox.AnyRequestFilter</filter-class>
        <async-supported>true</async-supported>
    </filter>

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

    <filter>
        <filter-name>forwardFilter</filter-name>
        <filter-class>com.wrox.AnyRequestFilter</filter-class>
        <async-supported>true</async-supported>
    </filter>

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

    <filter>
        <filter-name>asyncFilter</filter-name>
        <filter-class>com.wrox.AnyRequestFilter</filter-class>
        <async-supported>true</async-supported>
    </filter>

    <filter-mapping>
        <filter-name>asyncFilter</filter-name>
        <url-pattern>/*</url-pattern>
        <dispatcher>ASYNC</dispatcher>
    </filter-mapping>

 

下例的NonAsyncServlet将响应访问URL/regular的请求转发至nonAsync.jsp视图。

@WebServlet(name = "nonAsyncServlet", urlPatterns = "/regular")
public class NonAsyncServlet extends HttpServlet
{
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException
    {
        System.out.println("Entering NonAsyncServlet.doGet().");
        request.getRequestDispatcher("/WEB-INF/jsp/view/nonAsync.jsp")
                .forward(request, response);
        System.out.println("Leaving NonAsyncServlet.doGet().");
    }
}

 

下例所示的代码要复杂得多。为了使日志更加清晰,它为当前请求生成了一个唯一ID。如果unwrap参数不存在,它将使用startAsync(ServletRequesServletRespons)启动AsyncContext。这将是AsyncContext得到doGet方法中传入的请求和响应对象。如果过滤器封装了请求或响应,AsyncContext将使用封装后的对象。不过,如果存在unwrap参数,doGet将使用startAsync()方法启动AsyncContext。此时,AsyncContext将得到原始的请求和响应对象,而不是封装过的请求和响应。

注意对AsyncContextstart(Runnable)方法的调用。使用该方法告诉容器在它的内部线程池中运行Runnable。也可以启动自己的线程,但是用容器的线程池更安全,并且可以避免资源耗尽的问题。

@WebServlet(name = "asyncServlet", urlPatterns = "/async", asyncSupported = true)
public class AsyncServlet extends HttpServlet
{
    private static volatile int ID = 1;

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException
    {
        final int id;
        synchronized(AsyncServlet.class)
        {
            id = ID++;
        }
        long timeout = request.getParameter("timeout") == null ?
                10_000L : Long.parseLong(request.getParameter("timeout"));

        System.out.println("Entering AsyncServlet.doGet(). Request ID = " + id +
                ", isAsyncStarted = " + request.isAsyncStarted());

        final AsyncContext context = request.getParameter("unwrap") != null ?
                request.startAsync() : request.startAsync(request, response);
        context.setTimeout(timeout);

        System.out.println("Starting asynchronous thread. Request ID = " + id +
                ".");

        AsyncThread thread = new AsyncThread(id, context);
        context.start(thread::doWork);

        System.out.println("Leaving AsyncServlet.doGet(). Request ID = " + id +
                ", isAsyncStarted = " + request.isAsyncStarted());
    }

    private static class AsyncThread
    {
        private final int id;
        private final AsyncContext context;

        public AsyncThread(int id, AsyncContext context)
        {
            this.id = id;
            this.context = context;
        }

        public void doWork()
        {
            System.out.println("Asynchronous thread started. Request ID = " +
                    this.id + ".");

            try {
                Thread.sleep(5_000L);
            } catch (Exception e) {
                e.printStackTrace();
            }

            HttpServletRequest request =
                    (HttpServletRequest)this.context.getRequest();
            System.out.println("Done sleeping. Request ID = " + this.id +
                    ", URL = " + request.getRequestURL() + ".");

            this.context.dispatch("/WEB-INF/jsp/view/async.jsp");

            System.out.println("Asynchronous thread completed. Request ID = " +
                    this.id + ".");
        }
    }
}

 

(1)在浏览器中访问http://localhost:8080/filters/regular。在调试器的输出窗口中将出现下面的信息。注意normalFilter拦截Servlet请求,forwardFilter拦截JSP的转发请求。

Entering normalFilter.doFilter().
Entering NonAsyncServlet.doGet().
Entering forwardFilter.doFilter().
In nonAsync.jsp.
Leaving forwardFilter.doFilter().
Leaving NonAsyncServlet.doGet().
Leaving normalFilter.doFilter().

(2)在浏览器中访问http://localhost:8080/filters/async。调试器输出中将显示以下信息。注意normalFilter拦截该请求,但在响应发出之前就会完成。

Entering normalFilter.doFilter().
Entering AsyncServlet.doGet(). Request ID = 1, isAsyncStarted = false
Starting asynchronous thread. Request ID = 1.
Leaving AsyncServlet.doGet(). Request ID = 1, isAsyncStarted = true
Leaving normalFilter.doFilter(), async context holds wrapped request/response=true
Asynchronous thread started. Request ID = 1.

等待了5秒之后,AsyncThread内部类将响应发送到用户,调试器输出中将显示出下面的信息。当使用AsyncContextdispatch方法将请求派发到JSP中时,asyncFilter将拦截访问JSP的内部请求。

Done sleeping. Request ID = 1, URL = http://localhost:8080/filters/async.
Asynchronous thread completed. Request ID = 1.
Entering asyncFilter.doFilter().
In async.jsp.
Leaving asyncFilter.doFilter().

(3)访问http://localhost:8080/filters/async?unwrap,并等待响应完成。调试器输出中将立即显示出下面的信息(某些将在5秒之后显示)。此时,AsyncContext使用的是原始请求和响应,而不是封装请求和响应(粗体部分的输出发生了变化)。

Entering normalFilter.doFilter().
Entering AsyncServlet.doGet(). Request ID = 2, isAsyncStarted = false
Starting asynchronous thread. Request ID = 2.
Leaving AsyncServlet.doGet(). Request ID = 2, isAsyncStarted = true
Leaving normalFilter.doFilter(), async context holds wrapped request/response=false
Asynchronous thread started. Request ID = 2.
Done sleeping. Request ID = 2, URL = http://localhost:8080/filters/async.
Asynchronous thread completed. Request ID = 2.
Entering asyncFilter.doFilter().
In async.jsp.
Leaving asyncFilter.doFilter().

访问地址http://localhost:8080/filters/async?timeout=3000。调试器输出中将显示下面的消息,在5秒的睡眠之后,代码将从AsyncContext中获取请求,并最终导致IllegalStateException异常。这是因为AsyncContext超时过期了,响应也在AsyncThread内部类完成工作之前关闭了。

Entering normalFilter.doFilter().
Entering AsyncServlet.doGet(). Request ID = 3, isAsyncStarted = false
Starting asynchronous thread. Request ID = 3.
Leaving AsyncServlet.doGet(). Request ID = 3, isAsyncStarted = true
Leaving normalFilter.doFilter(), async context holds wrapped request/response=true
Asynchronous thread started. Request ID = 3.

 

9.4 调查过滤器的实际用例

9.4.1 添加简单的日志过滤器

RequestLogFilter类是处理应用程序的所有请求的过滤器链中的第一个过滤器。它将记录处理请求的时间,并记录所有访问应用程序的请求信息-IP地址、时间戳、请求方法、协议、响应状态和长度以及处理请求的时间-类似于Apache HTTP日志格式。日志操作被添加到finally块中,这样过滤器链中抛出的任何异常都不会阻止日志语句的执行。

public class RequestLogFilter implements Filter
{
    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain) throws IOException, ServletException
    {
        Instant time = Instant.now();
        StopWatch timer = new StopWatch();
        try
        {
            timer.start();
            chain.doFilter(request, response);
        }
        finally
        {
            timer.stop();
            HttpServletRequest in = (HttpServletRequest)request;
            HttpServletResponse out = (HttpServletResponse)response;
            String length = out.getHeader("Content-Length");
            if(length == null || length.length() == 0)
                length = "-";
            System.out.println(in.getRemoteAddr() + " - - [" + time + "]" +
                    " \"" + in.getMethod() + " " + in.getRequestURI() + " " +
                    in.getProtocol() + "\" " + out.getStatus() + " " + length +
                    " " + timer);
        }
    }

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

    @Override
    public void destroy() { }
}

 

9.4.2 使用过滤器亚索响应内容

在考虑压缩响应时,你可能认为应该执行过滤器链,然后在返回的过程中执行压缩逻辑。不过要记住:响应数据可以再Servlet完成请求处理之前返回到客户端。在异步请求处理的情况下,它还可以在Servlet完成请求处理之后返回到客户端。因此,如果希望修改响应内容,必须在传递响应对象到过滤器链之前对它进行封装。

public class CompressionFilter implements Filter
{
    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain) throws IOException, ServletException
    {
        if(((HttpServletRequest)request).getHeader("Accept-Encoding")
                .contains("gzip"))
        {
            System.out.println("Encoding requested.");
            ((HttpServletResponse)response).setHeader("Content-Encoding", "gzip");
            ResponseWrapper wrapper =
                    new ResponseWrapper((HttpServletResponse)response);
            try
            {
                chain.doFilter(request, wrapper);
            }
            finally
            {
                try {
                    wrapper.finish();
                } catch(Exception e) {
                    e.printStackTrace();
                }
            }
        }
        else
        {
            System.out.println("Encoding not requested.");
            chain.doFilter(request, response);
        }
    }

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

    @Override
    public void destroy() { }

    private static class ResponseWrapper extends HttpServletResponseWrapper
    {
        private GZIPServletOutputStream outputStream;
        private PrintWriter writer;

        public ResponseWrapper(HttpServletResponse request)
        {
            super(request);
        }

        @Override
        public synchronized ServletOutputStream getOutputStream()
                throws IOException
        {
            if(this.writer != null)
                throw new IllegalStateException("getWriter() already called.");
            if(this.outputStream == null)
                this.outputStream =
                        new GZIPServletOutputStream(super.getOutputStream());
            return this.outputStream;
        }

        @Override
        public synchronized PrintWriter getWriter() throws IOException
        {
            if(this.writer == null && this.outputStream != null)
                throw new IllegalStateException(
                        "getOutputStream() already called.");
            if(this.writer == null)
            {
                this.outputStream =
                        new GZIPServletOutputStream(super.getOutputStream());
                this.writer = new PrintWriter(new OutputStreamWriter(
                        this.outputStream, this.getCharacterEncoding()
                ));
            }
            return this.writer;
        }

        @Override
        public void flushBuffer() throws IOException
        {
            if(this.writer != null)
                this.writer.flush();
            else if(this.outputStream != null)
                this.outputStream.flush();
            super.flushBuffer();
        }

        @Override
        public void setContentLength(int length) { }

        @Override
        public void setContentLengthLong(long length) { }

        @Override
        public void setHeader(String name, String value)
        {
            if(!"content-length".equalsIgnoreCase(name))
                super.setHeader(name, value);
        }

        @Override
        public void addHeader(String name, String value)
        {
            if(!"content-length".equalsIgnoreCase(name))
                super.setHeader(name, value);
        }

        @Override
        public void setIntHeader(String name, int value)
        {
            if(!"content-length".equalsIgnoreCase(name))
                super.setIntHeader(name, value);
        }

        @Override
        public void addIntHeader(String name, int value)
        {
            if(!"content-length".equalsIgnoreCase(name))
                super.setIntHeader(name, value);
        }

        public void finish() throws IOException
        {
            if(this.writer != null)
                this.writer.close();
            else if(this.outputStream != null)
                this.outputStream.finish();
        }
    }

    private static class GZIPServletOutputStream extends ServletOutputStream
    {
        private final ServletOutputStream servletOutputStream;
        private final GZIPOutputStream gzipStream;

        public GZIPServletOutputStream(ServletOutputStream servletOutputStream)
                throws IOException
        {
            this.servletOutputStream = servletOutputStream;
            this.gzipStream = new GZIPOutputStream(servletOutputStream);
        }

        @Override
        public boolean isReady()
        {
            return this.servletOutputStream.isReady();
        }

        @Override
        public void setWriteListener(WriteListener writeListener)
        {
            this.servletOutputStream.setWriteListener(writeListener);
        }

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

        @Override
        public void close() throws IOException
        {
            this.gzipStream.close();
        }

        @Override
        public void flush() throws IOException
        {
            this.gzipStream.flush();
        }

        public void finish() throws IOException
        {
            this.gzipStream.finish();
        }
    }
}

 

首先,代码检查客户端是否在Accpet-Encoding请求头中包含了“gzip”编码。这是非常重要的检查,因为如果没有该设置,就意味着客户端可能无法处理gzip压缩响应。如果有,它将把Content-Encoding头设置为“gzip”,然后使用私有内部类ResponseWrapper的实例封装响应对象。

接下来,该类使用私有内部类GZIPServletOutputStream封装PrintWriter或者ServletOutputStream,通过它们将数据发送到客户端。该封装对象包含了一个GZIPOutputStream的内部示例。响应数据首先被写入GZIPOutputStream,当请求完成时,他将完成压缩并将压缩响应写入封装的ServletOutputStream中。ResponseWrapper还将组织Servlet代码设置响应的内容长度头,因为直到响应被压缩之后它才能获得内容长度。

(1)浏览器中访问地址http://localhost:8080/compression/ 和 http://localhost:8080/compression/servlet。

(2)使用浏览器的开发者工具监控应用程序的请求和响应头。可以看到如下界面。

 

9.5 使用过滤器简化认证

以下代码演示了如何使用监听器类和过滤器类,将认证用户是否登录的检查加入到所有的Servlet和需要受保护的资源。

@WebListener
public class Configurator implements ServletContextListener
{
    @Override
    public void contextInitialized(ServletContextEvent event)
    {
        ServletContext context = event.getServletContext();

        FilterRegistration.Dynamic registration = context.addFilter(
                "authenticationFilter", new AuthenticationFilter()
        );
        registration.setAsyncSupported(true);
        registration.addMappingForUrlPatterns(
                null, false, "/tickets", "/sessions"
        );
    }

    @Override
    public void contextDestroyed(ServletContextEvent event) { }
}

 

该监听器的内容非常简单,就是声明了AuthenticationFilter并将它映射到/tickets和/sessions。

public class AuthenticationFilter implements Filter
{
    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain) throws IOException, ServletException
    {
        HttpSession session = ((HttpServletRequest)request).getSession(false);
        if(session == null || session.getAttribute("username") == null)
            ((HttpServletResponse)response).sendRedirect("login");
        else
            chain.doFilter(request, response);
    }

    @Override
    public void init(FilterConfig config) throws ServletException { }

    @Override
    public void destroy() { }
}

AuthenticationFilter将在所有HTTP方法的请求上执行认证检查,如果用户并未登录就将用户重定向至登录界面。

采用这种方式的优势在于:如果要修改认证算法,只需要修改过滤器就可以保护应用程序中的资源。之前,你将不得不修改所有有关的Servlet。

 

 

Chapter 9
Updated on 9/4/14.

195.08 KB

Click to Download

 

 

摘录自:[美]Nicholas S.Williams著,王肖峰译 Java Web高级编程 [M]、清华大学出版社,2015、211-231、

posted @ 2016-11-23 22:42  guqiangjs  阅读(757)  评论(0)    收藏  举报