2014年8月4日

Jetty - Part 1/2

隨著用了越來越久的 Apache Tomcat,Tomcat 似乎也跟隨著 JDK 的腳步,越來越龐大,這時候,不妨考慮試試看,把原本用在 Tomcat 的 webapp,改成使用 Jetty 來運作。

Jetty目前已經是 9.2 版,除了能像 Tomcat 一樣 stand alone 運作,再將 application 放到 deployment 的 webapps 目錄中,最重要的是能像一般執行 Java Application 一樣,將 webapp server 以 Embedded 的方式啟動。

關於 Embedded Jetty 的文章並不多,有可能是使用的人還不多,另外有個問題,網路上找到的介紹文章,也會因為 Jetty 版本的更新,舊的寫法可能就沒有用了,這對 open source project 來說是個致命傷,使用 Tomcat 基本上就比較不會有這樣的問題,網路上隨手搜尋到的資料,通常都是正確且可以使用的。

因此學習的過程,就是參考官方網頁的 Embedding Jetty Tutorial,以及使用最新版的 sample code,沒有別的方法。

取得 Jetty

我們在 Jetty 官方網頁 下載 頁面中,只能下載到完整的 Jetty (ex: jetty-distribution-9.2.2.v20140723.zip),但是因為我們並不是要 stand alone 執行 Jetty,而是要用 Embedded 的方式,因此我們需要一個更方便使用的 Jetty jar file,把所有跟 Jetty 有關的程式都集中到一個 jar 檔裡面,我們可以到 Maven jetty-all 頁面中,下載這樣的 jar 檔 (ex: jetty-all-9.2.2.v20140723.jar)。

Simplest Server

最簡單的 Server 就是把網頁服務的 Port 啟動,其他什麼事都不做。以下的程式會啟動 TCP Port 8080 作為 HTTP 的 service port。

public class SimplestServer
{
    public static void main(String[] args) throws Exception
    {
        Server server = new Server(8080);
        server.start();
        server.join();
    }
}

接下來,我們要進一步撰寫提供網頁服務的處理器 Handler,以下的 HelloHandler 很單純地就是產生一個 HTML 網頁資料,並寫上 HelloWorld

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.handler.AbstractHandler;

public class HelloHandler extends AbstractHandler {
    public void handle(String target, Request baseRequest,
            HttpServletRequest request, HttpServletResponse response)
            throws IOException, ServletException {
        response.setContentType("text/html;charset=utf-8");
        response.setStatus(HttpServletResponse.SC_OK);
        baseRequest.setHandled(true);
        response.getWriter().println("<h1>Hello World</h1>");
    }
}

為了讓 HelloHandler 產生作用,我們必須調整剛剛的 Server 程式,以 setHandler 將 HelloHandler 指定給 server 使用。

public class SimplestServer {
    public static void main(String[] args) throws Exception {
        Server server = new Server(8080);
        server.setHandler(new HelloHandler());

        server.start();
        server.join();
    }
}

這時候如果啟動 SimplestServer,就可以用瀏覽器連結 http://localhost:8080/ 看到網頁。

ManyConnectors

一個基本的 web application server 必須要能支援 HTTP 與 HTTPS 兩種協定,因此我們接下來參考 ManyConnectors.java ,將 HTTP 與 HTTPS 環境建立起來。

首先,我們用 JDK 提供的 keytool 產生一個新的 ssl key。

keytool -genkey -dname "cn=maxkitcn, ou=maxkitou, o=maxkit, c=TW" -keyalg RSA -alias server -keypass yourkeypassword -keystore d:\temp\maxkitkeystore -storepass yourstorepassword -validity 36500

KeyStore javadoc 裡面提到,KeyStore Type 有兩種:JKS 或是 PKCS12,我們剛用 keytool 建立的是 PKCS12,這跟 Jetty 的預設 type 是不同的,因此 ManyConnector 裡面啟用 ssl 的方式,必須要做些微的調整。

另外為了增加程式的彈性,我們把 Port 跟 keystore 的一些設定值放到獨立的 properties 檔案中。

首先是設定檔 server.properties

jetty.port=8080
jetty.ssl.port=8443
jetty.ssl.keystore=/maxkitkeystore
jetty.ssl.keystore.storepass=yourkeypassword

另外也將 maxkitkeystore 檔案放在 src 目錄中。

ServerProperties.java 用來讀取 server.properties 檔案的設定。

public class ServerProperties extends Properties {
    private static final String PROPERTIES = "/server.properties";

    private Properties properties = new Properties();

    public Map<Object, Object> getProperties() {
        InputStream stream = this.getClass().getResourceAsStream(PROPERTIES);
        try {
            properties.load(stream);
        }
        catch (IOException ex) {
            ex.printStackTrace();
        }
        return Collections.unmodifiableMap(properties);
    }
}

ManyConnectors.java 設定 http 與 https,並沿用剛剛的 HelloHandler 作為提供網頁資料的處理器。這邊要注意,新舊版 ManyConnectors 範例程式的寫法是完全不一樣的,要注意看一下找到的資料是支援 Jetty 那一個版本。

import java.security.KeyStore;
import java.util.Map;

import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.SecureRequestCustomizer;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.SslConnectionFactory;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.webapp.WebAppContext;

public class ManyConnectors {
    private static final Map<?, ?> properties = new ServerProperties()
            .getProperties();

    private static final String DEFAULT_PORT = String.valueOf(properties
            .get("jetty.port"));
    private static final String DEFAULT_SSL_PORT = String.valueOf(properties
            .get("jetty.ssl.port"));
    private static final String SSL_KEYSTORE_FILE = String.valueOf(properties
            .get("jetty.ssl.keystore"));
    private static final String SSL_KEYSTORE_PASS = String.valueOf(properties
            .get("jetty.ssl.keystore.storepass"));

    private static Server server;

    public void startServer() throws Exception {
        startServer(DEFAULT_PORT, DEFAULT_SSL_PORT);
    }

    public void startServer(String port, String sslPort) throws Exception {
        server = new Server();
        logger.info("Starting Server...");
        // HttpConfiguration 告訴 server 要支援 https
        HttpConfiguration http_config = new HttpConfiguration();
        http_config.setSecureScheme("https");
        http_config.setSecurePort((sslPort.isEmpty() ? Integer
                .valueOf(DEFAULT_SSL_PORT) : Integer.valueOf(sslPort)));
        http_config.setOutputBufferSize(32768);

        // HTTP connector
        ServerConnector http = new ServerConnector(server,
                new HttpConnectionFactory(http_config));
        http.setPort((port.isEmpty() ? Integer.valueOf(DEFAULT_PORT) : Integer
                .valueOf(port)));
        http.setIdleTimeout(30000);

        // SSL Context Factory for HTTPS
        SslContextFactory sslContextFactory = new SslContextFactory();
        sslContextFactory.setKeyStorePassword(SSL_KEYSTORE_PASS);

        KeyStore keyStore = KeyStore.getInstance("PKCS12");
        keyStore.load(this.getClass().getResourceAsStream(SSL_KEYSTORE_FILE),
                SSL_KEYSTORE_PASS.toCharArray());
        sslContextFactory.setKeyStore(keyStore);

        // HTTPS Configuration
        HttpConfiguration https_config = new HttpConfiguration(http_config);
        https_config.addCustomizer(new SecureRequestCustomizer());

        // HTTPS connector
        ServerConnector https = new ServerConnector(server,
                new SslConnectionFactory(sslContextFactory, "http/1.1"),
                new HttpConnectionFactory(https_config));
        https.setPort((sslPort.isEmpty() ? Integer.valueOf(DEFAULT_SSL_PORT)
                : Integer.valueOf(sslPort)));
        https.setIdleTimeout(500000);

        Connector[] connectors = new Connector[] { http, https };

        server.setConnectors(connectors);

        server.setHandler(new HelloHandler());

        server.start();
        server.join();

        logger.info("Started Server");
    }

    public void stopServer() throws Exception {
        logger.info("Stopping Server...");
        server.stop();
        logger.info("Server Stopped with stopServer() method.");
    }
}

最後寫一個啟動的 Main 程式 SimplestServer2.java

public class SimplestServer2 {
    public static void main(String[] args) throws Exception {
        ManyConnectors server = new ManyConnectors();
        server.startServer();
    }
}

啟動 server 後,就可以用 http://localhost:8080/ 或是 https://localhost:8443/ 看到 HelloWorld 網頁。