2

Apache HttpClient 5 笔记: SSL, Proxy 和 Multipart Upload - Milton

 1 year ago
source link: https://www.cnblogs.com/milton/p/17017446.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Apache HttpClient 5

最近要在非SpringBoot环境调用OpenFeign接口, 需要用到httpclient, 注意到现在 HttpClient 版本已经到 5.2.1 了. 之前在版本4中的一些方法已经变成 deprecated, 于是将之前的工具类升级一下, 顺便把中间遇到的问题记录一下

基础使用方法

首先参考Apache官方的快速开始 httpcomponents-client-5.2.x quickstart, 这是页面上给的例子

Post请求

try (CloseableHttpClient httpclient = HttpClients.createDefault()) { HttpPost httpPost = new HttpPost("http://httpbin.org/post"); List<NameValuePair> nvps = new ArrayList<>(); nvps.add(new BasicNameValuePair("username", "vip")); nvps.add(new BasicNameValuePair("password", "secret")); httpPost.setEntity(new UrlEncodedFormEntity(nvps)); try (CloseableHttpResponse response2 = httpclient.execute(httpPost)) { System.out.println(response2.getCode() + " " + response2.getReasonPhrase()); HttpEntity entity2 = response2.getEntity(); // do something useful with the response body // and ensure it is fully consumed EntityUtils.consume(entity2); }}

Get请求

try (CloseableHttpClient httpclient = HttpClients.createDefault()) { HttpGet httpGet = new HttpGet("http://httpbin.org/get"); try (CloseableHttpResponse response1 = httpclient.execute(httpGet)) { System.out.println(response1.getCode() + " " + response1.getReasonPhrase()); HttpEntity entity1 = response1.getEntity(); // do something useful with the response body // and ensure it is fully consumed EntityUtils.consume(entity1); }}

替换 Deprecated 的 execute 方法

上面的例子可以正常运行, 但是在HttpClient5中, CloseableHttpResponse execute(ClassicHttpRequest request) 这个方法已经被标记为 Deprecated

@DeprecatedHttpResponse execute(ClassicHttpRequest var1) throws IOException; @DeprecatedHttpResponse execute(ClassicHttpRequest var1, HttpContext var2) throws IOException; @DeprecatedClassicHttpResponse execute(HttpHost var1, ClassicHttpRequest var2) throws IOException; @DeprecatedHttpResponse execute(HttpHost var1, ClassicHttpRequest var2, HttpContext var3) throws IOException;

取而代之的, 是这四个带 HttpClientResponseHandler 参数的 execute 方法

<T> T execute(ClassicHttpRequest var1, HttpClientResponseHandler<? extends T> var2) throws IOException;<T> T execute(ClassicHttpRequest var1, HttpContext var2, HttpClientResponseHandler<? extends T> var3) throws IOException;<T> T execute(HttpHost var1, ClassicHttpRequest var2, HttpClientResponseHandler<? extends T> var3) throws IOException;<T> T execute(HttpHost var1, ClassicHttpRequest var2, HttpContext var3, HttpClientResponseHandler<? extends T> var4) throws IOException;}

这个 HttpClientResponseHandler 作为响应的处理方法, 里面只有一个接口方法(要注意到这是一个函数式接口)

@FunctionalInterfacepublic interface HttpClientResponseHandler<T> { T handleResponse(ClassicHttpResponse var1) throws HttpException, IOException;}

JDK中提供了一个默认的实现, BasicHttpClientResponseHandler(基于 AbstractHttpClientResponseHandler)

public abstract class AbstractHttpClientResponseHandler<T> implements HttpClientResponseHandler<T> { public AbstractHttpClientResponseHandler() { } public T handleResponse(ClassicHttpResponse response) throws IOException { HttpEntity entity = response.getEntity(); if (response.getCode() >= 300) { EntityUtils.consume(entity); throw new HttpResponseException(response.getCode(), response.getReasonPhrase()); } else { return entity == null ? null : this.handleEntity(entity); } } public abstract T handleEntity(HttpEntity var1) throws IOException;} public class BasicHttpClientResponseHandler extends AbstractHttpClientResponseHandler<String> { public BasicHttpClientResponseHandler() { } public String handleEntity(HttpEntity entity) throws IOException { try { return EntityUtils.toString(entity); } catch (ParseException var3) { throw new ClientProtocolException(var3); } } public String handleResponse(ClassicHttpResponse response) throws IOException { return (String)super.handleResponse(response); }}

这样基础的使用方式就改为了下面的形式

public static void main(final String[] args) throws Exception { final CloseableHttpClient httpClient = HttpClients.createDefault(); try (httpClient) { final HttpGet httpGet = new HttpGet("https://www.baidu.com/home/other/data/weatherInfo"); final String responseBody = httpClient.execute(httpGet, new BasicHttpClientResponseHandler()); System.out.println(responseBody); }}

使用自定义的函数式方法处理响应

可以看到, 上面的 BasicHttpClientResponseHandler 是一个比较简单的实现, 大于300的响应状态码直接抛出异常, 其它的读出字符串. 这样的处理方式对于更精细的使用场景是不够的

  • 需要根据响应状态码判断时
  • 需要对响应解析为Java类时
  • 需要压住异常, 统一返回类对象时

之前在 HttpClient4 时, 可以通过CloseableHttpResponse response = client.execute(httpRequest), 对 response 进行判断, 现在response已经完全被handler 包裹, 需要通过自定义函数式方法处理响应, 看下面的例子

首先定义一个响应结果类, 数据部分使用泛型

@Datapublic class Client5Resp<T> implements Serializable { private int code; private String raw; private T data; public Client5Resp(int code, String raw, T data) { this.code = code; this.raw = raw; this.data = data; }}

然后对响应结果自定义 handler, 因为是函数式接口, 所以很方便在方法中直接定义, 处理的逻辑是:

  1. 如果 T 泛型为String, 直接将 body 作为数据返回
  2. 其它的 T 泛型, 用 Jackson 解开之后返回
  3. 将 status code 一并返回
private static <T> Client5Resp<T> httpRequest( TypeReference<T> tp, HttpUriRequest httpRequest) { try (CloseableHttpClient client = HttpClients.createDefault()) { Client5Resp<T> resp = client.execute(httpRequest, response -> { if (response.getEntity() != null) { String body = EntityUtils.toString(response.getEntity()); if (tp.getType() == String.class) { return new Client5Resp<>(response.getCode(), body, (T)body); } // 当需要区分更多类型时可以增加定义 else { T t = JacksonUtil.extractByType(body, tp); return new Client5Resp<>(response.getCode(), body, t); } } else { return new Client5Resp<>(response.getCode(), null, null); } }); log.info("rsp:{}, body:{}", resp.getCode(), resp.getRaw()); return resp; } catch (IOException|NoSuchAlgorithmException|KeyStoreException|KeyManagementException e) { // 当异常也需要返回 Client5Resp 类型对象时可以在catch中封装 log.error(e.getMessage(), e); } return null;}

自定义请求设置和Header

首先是超时设置

RequestConfig defaultRequestConfig = RequestConfig.custom() .setConnectionRequestTimeout(60, TimeUnit.SECONDS) .setResponseTimeout(60, TimeUnit.SECONDS) .build();

然后是 Header

List<Header> headers = List.of( new BasicHeader(HttpHeaders.CONTENT_TYPE, "application/json"), new BasicHeader(HttpHeaders.ACCEPT, "application/json"));

在 HttpGet/HttpPost 中设置

HttpGet httpGet = new HttpGet(...);if (headers != null) { for (Header header : headers) { httpGet.addHeader(header); }}RequestConfig requestConfig = RequestConfig.copy(defaultRequestConfig).build();httpGet.setConfig(requestConfig);

自定义 HttpClient 增加 SSL TrustAllStrategy

在 HttpClient5 中, 增加 SSL TrustAllStrategy 的方法也有变化, 这是获取 CloseableHttpClient 的代码

final RequestConfig defaultRequestConfig = RequestConfig.custom() .setConnectionRequestTimeout(60, TimeUnit.SECONDS) .setResponseTimeout(60, TimeUnit.SECONDS) .build();final BasicCookieStore defaultCookieStore = new BasicCookieStore();final SSLContext sslcontext = SSLContexts.custom() .loadTrustMaterial(null, new TrustAllStrategy()).build();final SSLConnectionSocketFactory sslSocketFactory = SSLConnectionSocketFactoryBuilder.create() .setSslContext(sslcontext).build();final HttpClientConnectionManager cm = PoolingHttpClientConnectionManagerBuilder.create() .setSSLSocketFactory(sslSocketFactory).build();return HttpClients.custom() .setDefaultCookieStore(defaultCookieStore) .setDefaultRequestConfig(defaultRequestConfig) .setConnectionManager(cm) .evictExpiredConnections() .build();

增加 Http Proxy

固定的 Proxy

在 HttpClient5 中, RequestConfig.Builder.setProxy()方法已经 Deprecated

@Deprecatedpublic RequestConfig.Builder setProxy(HttpHost proxy) { this.proxy = proxy; return this;}

需要使用 HttpClientBuilder.setRoutePlanner(HttpRoutePlanner routePlanner) 进行设置, 和SSL一起, 获取client的代码变成

final HttpHost proxy = new HttpHost(proxyHost, proxyPort);final DefaultProxyRoutePlanner routePlanner = new DefaultProxyRoutePlanner(proxy);return HttpClients.custom() .setDefaultCookieStore(defaultCookieStore) .setDefaultRequestConfig(defaultRequestConfig) .setRoutePlanner(routePlanner) .setConnectionManager(cm) .evictExpiredConnections() .build();

如果需要用户名密码, 需要再增加一个 CredentialsProvider, 变成

final HttpHost proxy = new HttpHost(proxyHost, proxyPort);final DefaultProxyRoutePlanner routePlanner = new DefaultProxyRoutePlanner(proxy);final BasicCredentialsProvider credsProvider = new BasicCredentialsProvider();credsProvider.setCredentials( new AuthScope(proxyHost, proxyPort), new UsernamePasswordCredentials(authUser, authPasswd.toCharArray())); return HttpClients.custom() .setDefaultCookieStore(defaultCookieStore) .setDefaultRequestConfig(defaultRequestConfig) .setDefaultCredentialsProvider(credsProvider) .setRoutePlanner(routePlanner) .setConnectionManager(cm) .evictExpiredConnections() .build();

动态 Proxy

如果需要随时切换 proxy, 需要自己实现一个 HttpRoutePlanner

public static class DynamicProxyRoutePlanner implements HttpRoutePlanner { private DefaultProxyRoutePlanner planner; public DynamicProxyRoutePlanner(HttpHost host){ planner = new DefaultProxyRoutePlanner(host); } public void setProxy(HttpHost host){ planner = new DefaultProxyRoutePlanner(host); } public HttpRoute determineRoute(HttpHost target, HttpContext context) throws HttpException { return planner.determineRoute(target, context); }}

然后在代码中进行切换

HttpHost proxy = new HttpHost("127.0.0.1", 1080);DynamicProxyRoutePlanner routePlanner = new DynamicProxyRoutePlanner(proxy);CloseableHttpClient httpclient = HttpClients.custom() .setRoutePlanner(routePlanner) .build();// 换代理routePlanner.setProxy(new HttpHost("192.168.0.1", 1081));

构造 Multipart 文件上传请求

首先是构造 HttpEntity 的方法, 这个方法中设置请求为 1个文件 + 多个随表单参数

public static HttpEntity httpEntityBuild(NameValuePair fileNvp, List<NameValuePair> nvps) { File file = new File(fileNvp.getValue()); MultipartEntityBuilder builder = MultipartEntityBuilder.create().setMode(HttpMultipartMode.STRICT); if (nvps != null && nvps.size() > 0) { for (NameValuePair nvp : nvps) { builder.addTextBody(nvp.getName(), nvp.getValue(), ContentType.DEFAULT_BINARY); } } builder.addBinaryBody(fileNvp.getName(), file, ContentType.DEFAULT_BINARY, fileNvp.getValue()); return builder.build();}
// 构造一个文件参数, 其它参数留空NameValuePair fileNvp = new BasicNameValuePair("sendfile", filePath);HttpEntity entity = httpEntityBuild(fileNvp, null);HttpPost httpPost = new HttpPost(api);httpPost.setEntity(entity); try (CloseableHttpClient client = getClient(...)) { Client5Resp<T> resp = client.execute(httpPost, response->{ ... });

注意: 在使用 HttpMultipartMode 时对 HttpEntity 设置 Header 要谨慎, 因为 HttpClient 会对 Content-Type增加 Boundary 后缀, 而这个是服务端判断文件边界的重要参数. 如果设置自定义 Header, 需要检查 boundary 是否正确生成. 如果没有的话需要自定义 Content-Type 将 boundary 加进去, 并且通过 EntityBuilder.setBoundary() 将自定义的 boundary 值传给 HttpEntity.

Links


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK