[Java, WLS] WLS 12.2.1 launch - Servlet 3.1 new features

原文はこちら。
https://blogs.oracle.com/WebLogicServer/entry/wls_12_2_1_launch

Introduction

WebLogic Server 12.2.1ではServlet 3.1仕様の新機能をサポートしています。Servlet 3.1仕様はServlet仕様のメジャーバージョンであり、このバージョンの仕様は、主にノンブロッキングIOとHTTPプロトコルのアップグレード機能をServletContainerに導入し、最新のWebアプリケーション開発に使えるようにしました。ノンブロッキングIOは、Webコンテナのスケーラビリティ改善への増大する要求への対策であるとともに、Webコンテナで同時に処理できる接続数を増やすことができます。ServletコンテナのノンブロッキングIOを使うと、開発者はデータが利用可能になるとデータを読めたり、可能であれば書きこんだりすることができます。また、このバージョンでは、セキュリティと機能強化のためのいくつかのマイナーな変更も導入しました。
JSR 340: Java Servlet 3.1 Specification
https://jcp.org/en/jsr/detail?id=340

1 Upgrade Processing

1.1 Description

HTTP/1.1では、general-headerのアップグレードを使うと、サポートされた利用したい追加の通信プロトコルをクライアントは指定することができます。サーバはプロトコルを切り替えることが適当と認める場合には、新しいプロトコルを使って後続の通信を実施します。
Servletコンテナは、HTTPアップグレードメカニズムを提供します。しかし、Servletコンテナ自体は、アップグレード対象のプロトコルに関する知識を有しません。プロトコル処理はHttpUpgradeHandlerにカプセル化されており、ServletコンテナとHttpUpgradeHandler間のデータの読み込みや書き込みはバイトストリームで行われます。
プロトコルのアップグレードリクエストを受信した場合、Servletは、HttpServletRequest.upgrade()メソッドを呼び出してアップグレードプロセスを開始することができます。このメソッドは、指定されたHttpUpgradeHandlerクラスをインスタンス化します。戻されたHttpUpgradeHandlerインスタンスをさらにカスタマイズすることができます。アプリケーションがクライアントに対して適切なレスポンス準備し、送信します。Servletのサービスメソッドを終了した後、Servletコンテナは、すべてのフィルタ処理を完了し、HttpUpgradeHandlerが処理するよう接続をマークします。そして、HttpUpgradeHandlerのinit()メソッドを呼び出し、プロトコルハンドラがデータストリームへアクセスできるよう、WebConnectionを渡します。
Servletフィルタは最初のHTTPリクエストとレスポンスを処理するだけで、その後の通信に関与しません。つまり、リクエストがアップグレードされると、呼び出されません。HttpUpgradeHandlerは、ノンブロッキングIOを利用して、メッセージを消費、生成することができます。HTTPアップグレードの処理中、開発者はServletInputStream、ServletOutputStreamへのスレッドセーフなアクセスのための責任を負います。アップグレード処理が完了すると、HttpUpgradeHandler.destroyが呼び出されます。

1.2 Example

この例では、クライアントがサーバに対してリクエストを送信し、サーバがリクエストを受け付け、レスポンスを返し、HttpUpgradeHandler.init()メソッドを呼び出してダミープロトコルを使って通信を継続します。クライアントはハンドシェイクの間、リクエスト・レスポンスヘッダを表示します。

Client

クライアントはHTTPアップグレードのリクエストを発行します。
@WebServlet(name = "ClientTest", urlPatterns = {"/"})
public class ClientTest extends HttpServlet {
  protected void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    String reqStr = "POST " + contextRoot + "/ServerTest HTTP/1.1" + CRLF;
    ...
    reqStr += "Upgrade: Dummy Protocol" + CRLF;

    // Create socket connection to ServerTest
    s = new Socket(host, port);
    input = s.getInputStream();
    output = s.getOutputStream();

    // Send request header with data
    output.write(reqStr.getBytes());
    output.flush();
  }
}
この例では、HTTP/1.1ヘッダフィールドをUpgrade: Dummy Protocolに変更しようとしています。さーば はプロトコルアップグレードのリクエストを受け入れるかどうかを判断します。

Server

ServerTest.javaはリクエストヘッダ中のUpgradeフィールドをチェックします。アップグレードリクエストを受け入れる場合、サーバはProtocolUpgradeHandler(HttpUpgradeHandlerの実装)を呼び出します。クライアントが指定したUpgradeプロトコルをサーバがサポートしない場合、404を返します。
@WebServlet(name="ServerTest", urlPatterns={"/ServerTest"})
public class ServerTest extends HttpServlet {
  protected void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    // Checking request header
    if ("Dummy Protocol".equals(request.getHeader("Upgrade"))){
    ...
    ProtocolUpgradeHandler handler = request.upgrade(ProtocolUpgradeHandler.class);
    } else {
      response.setStatus(400);
      ...
    }
  }
  ...
}
ProtocolUpgradeHandlerは、HttpUpgradeHandlerの実装であり、アップグレード要求を処理し、通信プロトコルを切り替えます。サーバはアップグレードヘッダの値をチェックし、そのプロトコルをサポートしているかどうかを判断します。サーバーがリクエストをを受け入れると、101(Switching Protocols)レスポンス中のUpgradeヘッダフィールドを使ってどのプロトコルに切り替わったかを示す必要があります。

Implementation of HttpUpgradeHandler

public class ProtocolUpgradeHandler implements HttpUpgradeHandler {
  @Override
  public void init(WebConnection wc) {
    this.wc = wc;
    try {
      ServletOutputStream output = wc.getOutputStream();
      ServletInputStream input = wc.getInputStream();
      Calendar calendar = Calendar.getInstance();
      DateFormat dateFormat = new SimpleDateFormat("MM/dd/yyyy HH:mm:ss");

      // Reading the data into byte array
      input.read(echoData);

      // Setting new protocol header
      String resStr = "Dummy Protocol/1.0 " + CRLF;
      resStr += "Server: Glassfish/ServerTest" + CRLF;
      resStr += "Content-Type: text/html" + CRLF;
      resStr += "Connection: Upgrade" + CRLF;
      resStr += "Date: " + dateFormat.format(calendar.getTime()) +CRLF;
      resStr += CRLF;

      // Appending data with new protocol
      resStr += new String(echoData) + CRLF;

      // Sending back to client
      ...
      output.write(resStr.getBytes());
      output.flush();
    } catch (IOException ex) {
      Logger.getLogger(ProtocolUpgradeHandler.class.getName()).log(Level.SEVERE, null, ex);
    }
    ...
  }

  @Override
  public void destroy() {
    ...
    try {
      wc.close();
    } catch (Exception ex) {
      Logger.getLogger(ProtocolUpgradeHandler.class.getName()).log(Level.SEVERE, "Failed to close connection", ex);
    }
    ...
  }
}
init()メソッドは、新しいプロトコル・ヘッダを設定します。新しいプロトコルはその後の通信に使用されます。この例では、ダミーのプロトコルを使用しています。アップグレードプロセスが完了すると、destroy()メソッドが呼び出されます。この例では、プロトコルのアップグレードのハンドシェイクプロセスを示しています。ハンドシェイクプロセスが終わると、後続の通信は、新しいプロトコルを使用します。このメカニズムは、既存のトランスポート層の接続上のアプリケーション層プロトコルのアップグレードにのみ適用されます。この機能は、Java EEプラットフォームプロバイダにとって最も有用です。

2 Non-blocking IO

2.1 Description


Webコンテナでのノンブロッキングリクエスト処理はWebコンテナのスケーラビリティ改善に対する増大する要望への改善策であり、Webコンテナが同時に処理可能な接続数を増やすことができます。ServletContainerでノンブロッキングIOを使うと、開発者はデータが読み取り可能になった時点、書き込み可能になった時点で操作できるようになります。ノンブロッキングIOはServletとFilter、アップグレード処理における非同期リクエスト処理でのみ利用可能です。それ以外の場合、ServletInputStreamのsetReadListenerを呼び出した場合にはIllegalStateExceptionがスローされなければなりません。

2.2 Non-Blocking Read Example

Servlet


ServerServletでは、サーバがリクエストを受け取り、リクエストの非同期処理を開始して、ReadListenerを登録しています。
@WebServlet(name = "ServerServlet", urlPatterns = {"/server"}, asyncSupported = true)
 public class ServerServlet extends HttpServlet {
      .....
   protected void service(HttpServletRequest request, HttpServletResponse response)
      throws ServletException, IOException {
    response.setContentType("text/html;charset=UTF-8");
    // async read
    final AsyncContext context = request.startAsync();
    final ServletInputStream input = request.getInputStream();
    final ServletOutputStream output = response.getOutputStream();
    input.setReadListener(new ReadListenerImpl(input, output, context));
   }
Note:
ノンブロッキングIOは、ServletおよびFilter、もしくはハンドラのアップグレードにおける非同期リクエスト処理でのみ利用できます。詳しくは、Servlet 3.1の仕様をご覧ください。

[訳注]
原文にはServlet Spec 3.2とありますが、Servlet 3.1の仕様としています。


Read Listener Implementation

public class ReadListenerImpl implements ReadListener {

  private ServletInputStream input;
  private ServletOutputStream output;
  private AsyncContext context;
  private StringBuilder sb = new StringBuilder();

  public ReadListenerImpl(ServletInputStream input, ServletOutputStream output, AsyncContext context) {
    this.input = input;
    this.output = output;
    this.context = context;
  }

  /**
   * do when data is available to be read.
   */
  @Override
  public void onDataAvailable() throws IOException {
    while (input.isReady()) {
      sb.append((char) input.read());
    }
  }

  /**
   * do when all the data has been read.
   */
  @Override
  public void onAllDataRead() throws IOException {
    try {
      output.println("ServerServlet has received '" + sb.toString() + "'.");
      output.flush();
    } catch (Exception e) {
      e.printStackTrace();
    } finally {
      context.complete();
    }
  }

  /**
   * do when error occurs.
   */
  @Override
  public void onError(Throwable t) {
    context.complete();
    t.printStackTrace();
  }
データが入力リクエストストリームから読み込み可能になると、onDataAvailable()メソッドを呼び出します。コンテナはその後、isReady()からtrueが返ってくる場合に限り、read()メソッドを呼び出します。リクエストのすべてのデータが読み込まれたときに、onAllDataRead()メソッドを呼び出します。 onError(Throwable t)メソッドは、リクエストの処理中にエラーや例外が発生する場合に呼び出されます。基礎となるデータストリームがブロックされていない場合、isReady()メソッドはtrueを返します。この時点で、コンテナはonDataAvailable()メソッドを呼び出します。
ユーザーは、異なるパラメータを処理するようにコンストラクタをカスタマイズすることができます。通常、パラメータはServletInputStream、ServletOutputStream、あるいはAsyncContextです。このサンプルでは、それらのすべてを使用して、ReadListenerインタフェースを実装しています。

2.3 Non-Blocking Write Example

Servlet


ServerServlet.javaでは、リクエストを受け取った後にServletが非同期リクエスト処理を開始し、WriteListenerを登録します。
protected void processRequest(HttpServletRequest request, HttpServletResponse response)
      throws ServletException, IOException {
    response.setContentType("text/html;charset=UTF-8");
    // async write
    final AsyncContext context = request.startAsync();
    final ServletOutputStream output = response.getOutputStream();
    output.setWriteListener(new WriteListenerImpl(output, context));
  }
Write Listener Implementation
public class WriteListenerImpl implements WriteListener {

  private ServletOutputStream output;
  private AsyncContext context;

  public WriteListenerImpl(ServletOutputStream output, AsyncContext context) {
    this.context = context;
    this.output = output;
  }

  /**
   * do when the data is available to be written
   */
  @Override
  public void onWritePossible() throws IOException {

    if (output.isReady()) {
      output.println("<p>Server is sending back 5 hello...</p>");
      output.flush();
    }

    for (int i = 1; i <= 5 && output.isReady(); i++) {
      output.println("<p>Hello " + i + ".</p>");
      output.println("<p>Sleep 3 seconds simulating data blocking.<p>");
      output.flush();

      // sleep on purpose
      try {
        Thread.sleep(3000);
      } catch (InterruptedException e) {
        // ignore
      }
    }
    output.println("<p>Sending completes.</p>");
    output.flush();
    context.complete();
  }

  /**
   * do when error occurs.
   */
  @Override
  public void onError(Throwable t) {
    context.complete();
    t.printStackTrace();
  }
}
データがレスポンスストリームに書き込めるようになると、onWritePossible()メソッドを呼び出しています。コンテナはその後、isReady()の結果がtrueである場合のみ、writeBytes()メソッドを呼び出します。レスポンスに書き込む間に何らかのエラーや例外が発生した場合には、onError(Throwable t)メソッドを呼び出します。基礎となるデータストリームがブロックされていない場合にisReady()メソッドはtrueを返します。この時点で、コンテナはwriteBytes()メソッドを呼び出します。

3 SessionID change

3.1 Description

Servlet 3.1仕様にはセッションの固定化を防ぐための新しいインターフェースやメソッドが導入されています。WebLogic ServerのServletContainerはセキュリティ上の理由で、セッションID変更処理が実装されています。

3.2 SessionID change Example

このサンプルアプリケーションでは、。SessionIDChangeListenerインターフェースがsessionIdChangedメソッドをオーバーライドしています。このメソッドはセッションのセッションIDの変更通知を受け取ります。SessionIDChangeTestは、javax.servlet.http.HttpServletRequest.changeSessionId()を呼び出してセッションIDの値を変更します。

Servlet

@WebServlet(name = "SessionIDChangeServlet", urlPatterns = {"/SessionIDChangeServlet"})
public class SessionIDChangeServlet extends HttpServlet {

  protected void processRequest(HttpServletRequest request, HttpServletResponse response)
      throws ServletException, IOException {
    response.setContentType("text/html;charset=UTF-8");
    PrintWriter out = response.getWriter();
    HttpSession session = request.getSession(true);

    try {

      StringBuilder sb = new StringBuilder();

      sb.append("<h3>Servlet SessionIDChangeTest at " + request.getContextPath() + "</h3><br/>");
      sb.append("<p>The current session id is: &nbsp;&nbsp;" + session.getId() + "</p>");

      /* Call changeSessionID() method. */
      request.changeSessionId();

      sb.append("<p>The current session id has been changed, now it is: &nbsp;&nbsp;" + session.getId() + "</p>");

      request.setAttribute("message", sb.toString());
      request.getRequestDispatcher("response.jsp").forward(request, response);

    } finally {
      out.close();
    }
  }
....
}


Servletはリクエストからセッションオブジェクトを取得します。sessionIDはそのタイミングで生成されます。request.changeSessionId()が呼び出された後、新たなsessionIDが生成され、セッションオブジェクトの古いsessionIDに置き換わります。

HttpSessionIdListener Implementation
@WebListener
public class SessionIDChangeListener implements HttpSessionIdListener {

  @Override
  public void sessionIdChanged(HttpSessionEvent event, String oldSessionId) {
    System.out.println("[Servlet session-id-change example] Session ID " + oldSessionId + " has been changed");
  }
}
request.changeSessionId()が呼び出されると、実装されているsessionIdChangedメソッドが起動します。

0 件のコメント:

コメントを投稿