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 的性能。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM