淺析 jdk11 中 HttpClient 的使用
零 前期準備
0 版本
JDK 版本 : OpenJDK 11.0.1
IDE : idea 2018.3
1 HttpClient 簡介
java.net.http.HttpClient 是 jdk11 中正式啟用的一個 http 工具類(其實早在 jdk9 的時候就已經存在了,只是處於孵化期),官方寓意為想要取代 HttpURLConnection 和 Apache HttpClient 等比較古老的開發工具。
新增的 HttpClient 截止到目前(2019年3月)為止其實網路資料還比較少,筆者只是根據一些博文和官方 Demo 自己摸索了一下,做了下總結。
【由於是 jdk11 中才正式使用的工具類,距離開發者還很遙遠,所以對於原始碼筆者暫不打算深挖,淺淺的理解怎麼使用就行】
一 HttpClient
在 Apache HttpClient 中,一般會建立一個 HttpClient 物件來作為門面。java.net.http.HttpClient 的邏輯也差不多,只是建立方式更加時髦了:
//建立 builder HttpClient.Builder builder = HttpClient.newBuilder(); //鏈式呼叫 HttpClient client = builder //http 協議版本1.1 或者 2 .version(HttpClient.Version.HTTP_2) //.version(HttpClient.Version.HTTP_1_1) //連線超時時間,單位為毫秒 .connectTimeout(Duration.ofMillis(5000)) //.connectTimeout(Duration.ofMinutes(1)) //連線完成之後的轉發策略 .followRedirects(HttpClient.Redirect.NEVER) //.followRedirects(HttpClient.Redirect.ALWAYS) //指定執行緒池 .executor(Executors.newFixedThreadPool(5)) //認證,預設情況下 Authenticator.getDefault() 是 null 值,會報錯 //.authenticator(Authenticator.getDefault()) //代理地址 //.proxy(ProxySelector.of(new InetSocketAddress("http://www.baidu.com", 8080))) //快取,預設情況下 CookieHandler.getDefault() 是 null 值,會報錯 //.cookieHandler(CookieHandler.getDefault()) //建立完成 .build();
在 builder() 方法中,最終會呼叫到 HttpClientImpl 的構造器,完成 HttpClient 的建立工作:
//HttpClientImpl.class private HttpClientImpl(HttpClientBuilderImpl builder, SingleFacadeFactory facadeFactory) { //CLIENT_IDS 是 AtomicLong 型別的變數,使用 incrementAndGet() 方法實現自增長的 id id = CLIENT_IDS.incrementAndGet(); //記錄下存有 id 的字串 dbgTag = "HttpClientImpl(" + id +")"; //ssl 認證 if (builder.sslContext == null) { try { sslContext = SSLContext.getDefault(); } catch (NoSuchAlgorithmException ex) { throw new InternalError(ex); } } else { sslContext = builder.sslContext; } //執行緒池,沒有的話就預設建立一個 Executor ex = builder.executor; if (ex == null) { ex = Executors.newCachedThreadPool(new DefaultThreadFactory(id)); isDefaultExecutor = true; } else { isDefaultExecutor = false; } delegatingExecutor = new DelegatingExecutor(this::isSelectorThread, ex); facadeRef = new WeakReference<>(facadeFactory.createFacade(this)); //處理 http 2 的 client 類 client2 = new Http2ClientImpl(this);‘ //快取操作 cookieHandler = builder.cookieHandler; //超時時間 connectTimeout = builder.connectTimeout; //轉發策略,預設為 NEVER followRedirects = builder.followRedirects == null ? Redirect.NEVER : builder.followRedirects; //代理設定 this.userProxySelector = Optional.ofNullable(builder.proxy); this.proxySelector = userProxySelector .orElseGet(HttpClientImpl::getDefaultProxySelector); if (debug.on()) debug.log("proxySelector is %s (user-supplied=%s)", this.proxySelector, userProxySelector.isPresent()); //認證設定 authenticator = builder.authenticator; //設定 http 協議版本 if (builder.version == null) { version = HttpClient.Version.HTTP_2; } else { version = builder.version; } if (builder.sslParams == null) { sslParams = getDefaultParams(sslContext); } else { sslParams = builder.sslParams; } //連線執行緒池 connections = new ConnectionPool(id); connections.start(); timeouts = new TreeSet<>(); //SelectorManager 本質上是 Thread 類的封裝 //selmgr 會開啟一條執行緒,HttpClient 的主要邏輯執行在此執行緒中 //所以說 HttpClient 是非阻塞的,因為並不跑在主執行緒中 try { selmgr = new SelectorManager(this); } catch (IOException e) { throw new InternalError(e); } //設定為守護執行緒 selmgr.setDaemon(true); filters = new FilterFactory(); initFilters(); assert facadeRef.get() != null; }
主要是一些儲存操作,大致理解即可,不細究。
二 HttpRequest
HttpRequest 是發起請求的主體配置:
//建立 builder HttpRequest.Builder reBuilder = HttpRequest.newBuilder(); //鏈式呼叫 HttpRequest request = reBuilder //存入訊息頭 //訊息頭是儲存在一張 TreeMap 裡的 .header("Content-Type", "application/json") //http 協議版本 .version(HttpClient.Version.HTTP_2) //url 地址 .uri(URI.create("http://openjdk.java.net/")) //超時時間 .timeout(Duration.ofMillis(5009)) //發起一個 post 訊息,需要存入一個訊息體 .POST(HttpRequest.BodyPublishers.ofString("hello")) //發起一個 get 訊息,get 不需要訊息體 //.GET() //method(...) 方法是 POST(...) 和 GET(...) 方法的底層,效果一樣 //.method("POST",HttpRequest.BodyPublishers.ofString("hello")) //建立完成 .build();
三 傳送
發起請求:
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
這是同步式的發起請求方式,先來看一下它的實現:
public <T> HttpResponse<T> send(HttpRequest req, BodyHandler<T> responseHandler) throws IOException, InterruptedException{ CompletableFuture<HttpResponse<T>> cf = null; try { //呼叫 sendAsync(...) 方法非同步地完成主邏輯,並獲取 Future cf = sendAsync(req, responseHandler, null, null); return cf.get(); //這之後的所有程式碼都是在進行異常捕捉,所以可以忽略 } catch (InterruptedException ie) { if (cf != null ) cf.cancel(true); throw ie; } catch (ExecutionException e) { final Throwable throwable = e.getCause(); final String msg = throwable.getMessage(); if (throwable instanceof IllegalArgumentException) { throw new IllegalArgumentException(msg, throwable); } else if (throwable instanceof SecurityException) { throw new SecurityException(msg, throwable); } else if (throwable instanceof HttpConnectTimeoutException) { HttpConnectTimeoutException hcte = new HttpConnectTimeoutException(msg); hcte.initCause(throwable); throw hcte; } else if (throwable instanceof HttpTimeoutException) { throw new HttpTimeoutException(msg); } else if (throwable instanceof ConnectException) { ConnectException ce = new ConnectException(msg); ce.initCause(throwable); throw ce; } else if (throwable instanceof IOException) { throw new IOException(msg, throwable); } else { throw new IOException(msg, throwable); } } }
本質上是使用了非同步實現方法 sendAsync(...)。
在 Demo 中也可以直接使用:
//返回的是 future,然後通過 future 來獲取結果 CompletableFuture<String> future = client.sendAsync(request, HttpResponse.BodyHandlers.ofString()) .thenApply(HttpResponse::body); //阻塞執行緒,從 future 中獲取結果 String body = future.get();
四 一點嘮叨
java.net.http.HttpClient 非常的年輕,網路資料不多,且程式碼非常精細和複雜,目前來看底層應該是使用了執行緒池搭配 Socket 進行非同步通訊。具體有待後續研究。