2015年10月14日 星期三

使用 Jersey client 送出 HTTP 要求

一般來說,要用 Java 送出 HTTP 要求時,大多直覺會想到的都是 Apache HttpClient 吧
不過在我們的環境中,遇到了一點奇妙的需求,Apache 的套件有點不合用
剛好又發現有網友提到,Jersey 本身就有提供 Jersey client 可以作為送出 HTTP 的方法
而且我們本來就用 Jersey 來提供 RESTful 介面,不需要另外引用函式庫,所以就拿來試試看了。


因為我們的環境中,是先從 RESTful 介面收到瀏覽器送來的要求後,轉送一份給另一台主機
所以下述的範例程式碼中,流程會以 RESTful 介面作為起點。

RESTful 介面的程式碼如下:
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import javax.ws.rs.ext.Provider;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Provider
@Path("")
public class Wrapper {
    private static Logger log = LoggerFactory.getLogger("Wrapper");
    private @Context HttpHeaders headers;
    private @Context UriInfo uriInfo;

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public Response getStatus() {
        log.debug("URI: " + uriInfo.getRequestUri().getQuery());
        return DefaultHttpProxy.get(headers, uriInfo);
    }
}

簡單來說,就是收到一個 HTTP 要求時,就把這個要求中夾帶的標頭和 URL 參數丟給 DefaultHttpProxy 這個類別。
而 DefaultHttpProxy 的程式碼如下:

import java.io.InputStream;
import java.util.Iterator;
import java.util.List;
import java.util.Map.Entry;

import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.Entity;
import javax.ws.rs.client.Invocation.Builder;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;

import org.glassfish.jersey.filter.LoggingFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * A default implementation for replaying the HTTP request to another RESTful service.
 * @author Wayne Yeh @ 2015-10-14 
 */
public class DefaultHttpProxy {
    private static Logger log = LoggerFactory.getLogger("DefaultHttpProxy");
    
    /**
     * Send GET request with specified headers and query parameters.
     * @param headers Request headers.
     * @param uriInfo Request URI.
     * @return Response from the target of proxy.
     */
    public static Response get (HttpHeaders headers, UriInfo uriInfo) {
        Builder httpBuilder = buildBaseRequest(headers, uriInfo);
        return httpBuilder.get();
    }
    
    private static Builder buildBaseRequest (HttpHeaders headers, UriInfo uriInfo) 
            throws IllegalArgumentException {
        
        if(uriInfo == null) throw new IllegalArgumentException("No target URI is specified.");
        
        // ----------------------------------------------------------------------------------------- //
        //       Initiate a HTTP request for replay the client's request to Elasticsearch.       //
        // ----------------------------------------------------------------------------------------- //
        
        // Initiate the request builder.
        Client httpClient = ClientBuilder.newClient();
        Builder httpBuilder = httpClient.target("http://localhost:9200/" + subUri).register(new LoggingFilter()).request();
        log.debug("Try to request {}", "http://localhost:9200/" + subUri);
        
        // Try to replay the headers to the request builder.
        if(headers != null) {
            // List the headers and replay them to the request builder.
            Iterator<Entry<String, List<String>>> headersIter = headers.getRequestHeaders().entrySet().iterator();
            while (headersIter.hasNext()) {
                Entry<String, List<String>> header = headersIter.next();
                
                // Extract each of the headers and put it to the request builder.
                for(String value : header.getValue()) {
                    log.debug("Extract request header {} : {}", header.getKey(), value);
                    httpBuilder.header(header.getKey(), value);
                }
            }
        }
        
        // Set that the API accept JSON.
        httpBuilder.accept(MediaType.APPLICATION_JSON_TYPE);
        
        return httpBuilder;
    }
}

其中真正在進行發送 HTTP 要求的是 46~67 行以及 34 行,流程大概是先建立一個新的 Client 實例
然後透過 target() 方法指定要發送的目標 URI
後面接的 register() 是要註冊一個 debug 用的 Filter,可以讓 Jersey client 在運作時印出 log
然後呼叫 request() 可以獲得一個 Builder 類別的實例,能夠做後續的 HTTP 要求的細部設定。

如果有需要指定標頭等資訊的話,可以透過 Builder.header() 方法來設定(51~64 行)
最後要送出要求時,因為這裡是用 GET 送出,因此就呼叫 Builder.get() 就可以送出去了(34 行)。
回應的會是 Jersey 一貫的 Response 物件~
在我們的案例中,本來也要包 Response 物件作為回給客戶端(瀏覽器)的回應
因此 Jersey client 幫我包好了也省了一番轉換的功夫!XD

在執行時,瀏覽器輸入 http://localhost:8080/?a=a
得到的 log 如下:
2015-10-14 14:56:28.888 | DEBUG | RequestFilter | Get request URI from http://localhost:8080/?a=a
2015-10-14 14:56:28.891 | DEBUG | ElasticsearchWrapper | Get is called.
2015-10-14 14:56:28.891 | DEBUG | ElasticsearchWrapper | URI: a=a
2015-10-14 14:56:29.001 | DEBUG | DefaultHttpProxy | Try to request http://localhost:9200/?a=a
2015-10-14 14:56:29.006 | DEBUG | DefaultHttpProxy | Extract request header User-Agent : Mozilla/5.0 (Windows NT 6.1; WOW64; rv:41.0) Gecko/20100101 Firefox/41.0
2015-10-14 14:56:29.006 | DEBUG | DefaultHttpProxy | Extract request header Connection : keep-alive
2015-10-14 14:56:29.006 | DEBUG | DefaultHttpProxy | Extract request header Host : localhost:8080
2015-10-14 14:56:29.007 | DEBUG | DefaultHttpProxy | Extract request header Accept-Language : zh-TW,zh;q=0.8,en-US;q=0.5,en;q=0.3
2015-10-14 14:56:29.007 | DEBUG | DefaultHttpProxy | Extract request header Accept-Encoding : gzip, deflate
2015-10-14 14:56:29.007 | DEBUG | DefaultHttpProxy | Extract request header Accept : text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Oct 14, 2015 2:56:29 PM org.glassfish.jersey.filter.LoggingFilter log
INFO: 1 * Sending client request on thread qtp2110121908-17
1 > GET http://localhost:9200/?a=a
1 > Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8,application/json
1 > Accept-Encoding: gzip, deflate
1 > Accept-Language: zh-TW,zh;q=0.8,en-US;q=0.5,en;q=0.3
1 > Connection: keep-alive
1 > Host: localhost:8080
1 > User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:41.0) Gecko/20100101 Firefox/41.0

Oct 14, 2015 2:56:29 PM org.glassfish.jersey.client.internal.HttpUrlConnector setOutboundHeaders
WARNING: Attempt to send restricted header(s) while the [sun.net.http.allowRestrictedHeaders] system property not set. Header(s) will possibly be ignored.
Oct 14, 2015 2:56:29 PM org.glassfish.jersey.filter.LoggingFilter log
INFO: 1 * Client response received on thread qtp2110121908-17
1 < 200
1 < Content-Length: 339
1 < Content-Type: application/json; charset=UTF-8

參考資料:
  1. Chapter 5. Client API
  2. Jersey: Print the actual request
  3. 27.21.2. Migrating Jersey Client API

沒有留言: