Servlet3.0 新特性

Servlet3.0 的注解

Servlet 允许开发人员采用注解的方式来配置 Servlet、Filter、Listener。

Servlet3.0 规范在 javax.servlet.annotation 包下提供了如下注解。

@WebServlet:用于修饰一个 Servlet 类,用于部署 Servlet 类。

@WebInitParam:用于与 @WebServlet 或 @WebFilter 一起使用,为 Servlet、Filter 配置参数。

@WebListener:用于修饰 Listener 类,用于部署 Listener 类

@WebFilter:用于修饰 Filter 类,用于部署 Filter 类

@MultipartConfig:用于修饰 Servlet,指定该 Servlet 将会负责处理 multipart/form-data 类型的请求(主要用于文件上传)

@ServletSecurity:这是一个与 JAAS 有关的注解,修饰 Servlet 指定该 Servlet 的安全与授权控制

@HttpConstraint:用于与 @ServletSecurity 一起使用,用于指定该 Servlet 的安全与授权控制

@HttpMethodConstraint:用于与 @ServletSecurity 一起使用,用于指定该 Servlet 的安全与授权控制

 

Servlet3.0 提供的异步处理

在以前的 Servlet 规范中,如果 Servlet 作为控制器调用了一个耗时的业务方法,那么 Servlet 必须等到业务完全返回之后才会生成响应,这将使得 Servlet 对业务方法的调用变成一种阻塞式的调用,因此效率比较低。

Servlet3.0 规范引入了异步处理来解决这个问题,异步处理允许 Servlet 重新发起一条新线程去调用耗时的业务方法,这样就可避免等待。

Servlet3.0 的异步处理是通过 AsyncContext 类来处理的,Servlet 可通过 ServletRequest 的如下两个方法开启异步调用、创建 AsyncContext 对象。

AsyncContext start()

AsyncContext startAsync(ServletRequest, ServletResponse)

重复调用上面的方法将得到同一个 AsyncContext 对象。AsyncContext 对象代表异步处理的上下文,它提供了一些工具方法,可完成设置异步调用的超时时长,dispatch 用于请求、启动后台线程、获取 request、response 对象等功能。

 

AsyncServlet.java

package com.baiguiren;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.Date;

import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.jsp.*;
import javax.servlet.annotation.*;

@WebServlet(urlPatterns="/async", asyncSupported=true)
public class AsyncServlet extends HttpServlet
{
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException
    {
        response.setContentType("text/html; charset=UTF-8");
        PrintWriter out = response.getWriter();
        out.println("<title>异步调用实例</title>");
        out.println("进入 Servlet 的时间:" + new Date() + ".<br/>");

        // 创建 AsyncContext,开始异步调用
        AsyncContext actx = request.startAsync();
        // 设置异步调用的超时时长
        actx.setTimeout(60 * 1000);
        // 启动异步调用的线程
        actx.start(new GetBookTask(actx));

        out.println("结束 Servlet 的时间:" + new Date() + ".<br/>");
        out.flush();
    }
}

  

GetBookTask.java

package com.baiguiren;

import java.util.ArrayList;
import java.util.List;

import javax.servlet.*;

public class GetBookTask implements Runnable
{
    private AsyncContext actx = null;

    public GetBookTask(AsyncContext actx)
    {
        this.actx = actx;
    }

    public void run()
    {
        try {
            // 等待 5 秒,以模拟业务的方法执行
            Thread.sleep(5 * 1000);
            ServletRequest request = actx.getRequest();
            List<String> books = new ArrayList<String>();
            books.add("book 1");
            books.add("book 2");
            books.add("book 3");
            request.setAttribute("books", books);
            actx.dispatch("/async.jsp");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

  

上面的 task 方法暂停 5 秒来模拟调用耗时的业务方法,最后调用 AsyncContext 的 dispatch 方法把请求 dispatch 到指定的 JSP 页面。

 

async.jsp

<%@ page contentType="text/html; charset=UTF-8" session="false" %>
<%@ taglib prefix="c" tagdir="/WEB-INF/tags" %>
<%@ page import="java.util.*" %>

<c:forEach items="${books}" />

<%
out.println("业务调用结束的时间:" + new java.util.Date());
if (request.isAsyncStarted()) {
    // 完成异步调用
    request.getAsyncContext().complete();
}
%>

  

forEach.tag

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

<%@ attribute name="items" required="true" type="java.util.List" %>

<ul>
    <%
    for (Object s : items) {
    %>
        <li><%=s%></li>
    <%
    }
    %>
</ul>

  

对于希望启用异步调用的 Servlet 而言,开发者必须显示指定开启异步调用,为 Servlet 开启异步调用有两种方式。

为 @WebServlet 指定 asyncSupported=true

在 web.xml 文件的 <servlet ../> 元素中增加 <async-supported .../> 子元素,该子元素的值为 true

 

对于支持异步调用的 Servlet 来说,当 Servlet 以异步方式启用新线程之后,该 Servlet 的执行并不会阻塞,该 Servlet 将可以向客户端浏览器生成响应 --- 当新线程执行完成后,新线程生成的响应再次被送往客户端浏览器。

 

当 Servlet 启用异步调用的线程之后,该线程的执行过程对开发者是透明的。但在有些情况下,开发者需要了解该异步线程的执行细节,并针对特定的执行结果进行针对性处理,这可借助于 Servlet3.0 提供的异步监听器来实现。

异步监听器需要实现 AsyncListener 接口,实现该接口的监听器类需要实现如下 4 个方法。

onStartAsync(AsyncEvent event):当异步调用开始时触发该方法

onComplete(AsyncEvent event):当异步调用完成时触发该方法

onError(AsyncEvent event):当异步调用出错时触发该方法

onTimeout(AsyncEvent event):当异步调用超时时触发该方法

 

MyAsyncListener.java

package com.baiguiren;

import java.io.IOException;
import java.util.Date;

import javax.servlet.*;
import javax.servlet.annotation.*;

@WebListener
public class MyAsyncListener implements AsyncListener
{
    public void onComplete(AsyncEvent event) throws IOException
    {
        System.out.println("---异步调用完成---" + new Date());
    }

    public void onError(AsyncEvent event) throws IOException
    {
    }

    public void onStartAsync(AsyncEvent event) throws IOException
    {
        System.out.println("---异步调用开始---" + new Date());
    }

    public void onTimeout(AsyncEvent event) throws IOException
    {
    }
}

  

虽然上面的 Listener 监听器类可以监听异步调用开始、异步调用完成两个时间,但从实际运行的结果来看,它并不能监听到异步调用开始事件,这可能是因为注册该监听器时异步调用已经开始了的缘故。

 

 

改进的 Servlet API

Servlet3.0 还有一个改变是改进了部分 API,这种改进很好地简化了 java web 开发。其中两个较大的改进是:

HttpServletRequest 增加了对文件上传的支持

ServletContext 允许通过编程的方式动态注册 Servlet、Filter。

 

HttpServletRequest 提供了如下两个方法来处理文件上传:

Part getPart(String name):根据名称来获取文件上传域

Collection<Part> getParts():获取所有的文件上传域

上面两个方法的返回值都涉及一个 API:Part,每个 Part 对象对应于一个文件上传域,该对象提供了大量方法来访问上传文件的文件类型、大小、输入流等,并提供了一个 write(String file) 方法将上传文件写入磁盘。

 

upload.jsp

<%@ page contentType="text/html; charset=UTF-8" %>

<html>
    <head>
        <title>文件上传</title>
    </head>
    <body>
        <form method="post" action="upload" enctype="multipart/form-data">
            文件名:<input type="text" id="name" name="name" /><br/>
            选择文件:<input type="file" id="file" name="file"/><br/>
            <input type="submit" value="上传" /><br/>
        </form>
    </body>
</html>

  

 

UploadServlet.java

package com.baiguiren;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.Collection;

import javax.servlet.*;
import javax.servlet.jsp.*;
import javax.servlet.http.*;
import javax.servlet.annotation.*;

@WebServlet(name="upload", urlPatterns={"/upload"})
@MultipartConfig
public class UploadServlet extends HttpServlet
{
    public void service(HttpServletRequest request, HttpServletResponse response)
        throws IOException, ServletException
    {
        response.setContentType("text/html; charset=UTF-8");
        PrintWriter out = response.getWriter();
        request.setCharacterEncoding("UTF-8");
        // 获取普通请求参数
        String name = request.getParameter("name");
        out.println("普通的 name 参数为:" + name + "<br/>");

        // 获取文件上传域
        Part part = request.getPart("file");
        // 获取上传文件的文件类型
        out.println("上传文件的类型为:" + part.getContentType() + "<br/>");
        // 获取上传文件的大小
        out.println("上传文件的大小为:" + part.getSize() + "<br/>");
        // 获取该文件上传域的 Header Name
        Collection<String> headerNames = part.getHeaderNames();
        // 遍历文件上传域的 Header Name、Value
        for (String headerName : headerNames)
        {
            out.println(headerName + "---->" + part.getHeader(headerName) + "<br/>");
        }
        // 获取包含文件原始名的字符串
        String fileNameInfo = part.getHeader("content-disposition");
        // 提取上传文件的原始文件名
        String fileName = fileNameInfo.substring(fileNameInfo.indexOf("filename=\"") + 10, fileNameInfo.length() - 1);
        // 将上传的文件写入服务器
        part.write(getServletContext().getRealPath("/uploadFiles") + "/" + fileName);
    }
}

  

上面 Servlet 使用了 @MultipartConfig 修饰,处理文件上传的 Servlet 应该使用该注解修饰。也可以在 web.xml 中的 <servlet.../> 子元素中添加 <multipart-config.../> 子元素来达到同样效果。

 

ServletContext 则提供了如下方法来动态地注册 Servlet、Filter,并允许动态设置 Web 应用的初始化参数。

多个重载的 addServlet() 方法:动态地注册 Servlet

多个重载的 addFilter() 方法:动态地注册 Filter

多个重载的 addListener() 方法:动态地注册 Listener

setInitParameter(String name, String value) 方法:为 web 应用设置初始化参数。

 

 

Servlet3.1 新增的非阻塞式 IO

Servlet 底层的 IO 是通过如下两个 IO 流来支持的。

ServletInputStream:Servlet 用于读取数据的输入流。

ServletOutputStream:Servlet 用于输出数据的输出流。

 

以 Servlet 读取数据为例,传统的读取方式采用阻塞式 IO -- 当 Servlet 读取浏览器提交的数据时,如果数据暂时不可用,或数据没有读取完成,Servlet 当前所在线程将会被阻塞,无法继续向下执行。

从 Servlet 3.1 开始,ServletInputStream 新增了一个 setReadListener(ReadListener readListener) 方法,该方法允许以非阻塞式 IO 读取数据,实现 ReadListener监听器需要实现如下三个方法。

onAllDataRead():当所有数据读取完成时触发该方法

onDataAvailable():当有数据可用时触发该方法

onError(Throwable t):当读取数据出现错误时触发该方法

 

类似地,ServletOutputStream 也提供了 setWriterListener(WriteListener writeListener) 方法,通过这种方式,可以让 ServletOutputStream 以非阻塞 IO 进行输出。

 

在 Servlet 中使用非阻塞 IO 非常简单,按如下步骤进行即可。

1、调用 ServletRequest 的 startAsync() 方法开启异步模式。

2、通过 ServletRequest 获取 ServletInputStream,并为 ServletInputStream 设置监听器(ReadListener 实现类)。

3、实现 ReadListener 接口实现监听器,在该监听器的方法中以非阻塞方式获取数据。

 

AsyncServlet1.java

package com.baiguiren;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.Date;

import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.jsp.*;
import javax.servlet.annotation.*;

@WebServlet(urlPatterns="/async1", asyncSupported=true, loadOnStartup=1)
public class AsyncServlet1 extends HttpServlet
{
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException
    {
        response.setContentType("text/html; charset=UTF-8");
        PrintWriter out = response.getWriter();
        out.println("<title>非阻塞 IO 实例</title>");
        out.println("进入 Servlet 的时间:" + new Date() + ".<br/>");

        // 创建 AsyncContext,开始异步调用
        AsyncContext actx = request.startAsync();
        // 设置异步调用的超时时长
        actx.setTimeout(60 * 1000);

        // 为输入流注册监听器
        ServletInputStream input = request.getInputStream();
        input.setReadListener(new MyReadListener(input, actx));

        out.println("结束 Servlet 的时间:" + new Date() + ".<br/>");
        out.flush();
    }
}

  

MyReadListener.java

package com.baiguiren;

import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.jsp.*;
import javax.servlet.annotation.*;

public class MyReadListener implements ReadListener
{
    private ServletInputStream input;

    private AsyncContext context;

    public MyReadListener(ServletInputStream input, AsyncContext context)
    {
        this.input = input;
        this.context = context;
    }

    @Override
    public void onDataAvailable()
    {
        System.out.println("数据可用!");

        try {
            // 暂停 5 秒,模拟耗时操作
            Thread.sleep(5000);
            StringBuilder sb = new StringBuilder();
            int len = -1;
            byte[] buff = new byte[1024];

            // 采用原始 IO 方式读取浏览器向 Servlet 提交的数据
            while (input.isReady() && (len = input.read(buff)) > 0)
            {
                String data = new String(buff, 0, len);
                sb.append(data);
            }

            System.out.println(sb);
            // 将数据设置为 request 范围的属性
            context.getRequest().setAttribute("info", sb.toString());

            // 转发到视图页面
            context.dispatch("/async1.jsp");
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

    @Override
    public void onAllDataRead()
    {
        System.out.println("数据读取完成");
    }

    @Override
    public void onError(Throwable t)
    {
        t.printStackTrace();
    }
}

  

async1.jsp

<%@ page contentType="text/html; charset=UTF-8" session="false" %>

<%
out.println("业务调用结束的时间:" + new Date());

out.println(request.getAttribute("info"));
if (request.isAsyncStarted()) {
    // 完成异步调用
    request.getAsyncContext().complete();
}
%>

  

上面程序中的 MyReadListener 的 onDataAvailable() 方法先暂停 5 秒,用于模拟耗时操作,接下来程序使用普通 IO 流读取浏览器提交的数据。

如果程序直接让 Servlet 读取浏览器提交的数据,那么该 Servlet 就需要阻塞 5 秒,不能继续向下执行;改为非阻塞 IO 进行读取,虽然读取数据的 IO 操作需要 5 秒,但它不会阻塞 Servlet 执行,因此可以提升 Servlet 的性能。

 

posted @ 2018-03-31 02:42  佚名000  阅读(1614)  评论(0编辑  收藏  举报