java.net.http
模块使用
引言
写代码的时候难免会远程调用别人的api,之前用httpclient
,在接着是okhttp
,也都是跟着项目上用的,其实java 9
就出现了一个http模块,只是当时是孵化版本,java 11
正式推出了.
简介
主要类和接口
- 类
java.net.http.HttpClient
java.net.http.HttpHeaders
java.net.http.HttpRequest
java.net.http.HttpRequest.BodyPublishers
java.net.http.HttpRequest.BodyHanler
java.net.http.HttpRequest.BodySubscribers
- 接口
java.net.http.HttpClient.Builder
java.net.http.HttpRequest.BodyPublisher
java.net.http.HttpRequest.Builder
java.net.http.HttpResponse<T>
java.net.http.HttpResponse.BodyHandler<T>
java.net.http.HttpResponse.BodySubscriber<T>
java.net.http.HttpResponse.PushPromiseHandler<T>
java.net.http.HttpResponse.ResponseInfo
java.net.http.WebSocket
java.net.http.WebSocket.Builder
java.net.http.WebSocket.Listener
基本使用
jdk 9
之后都是使用模块化组织代码,所以创建一个模块化的项目让后引入java.net.http
模块.1
2
3module com.dbj.httpClient{
requires java.net.http
}创建
httpClient
使用
builder
模式创建对象, 基本上该包下面所有的对象都使用builder
模式创建对象, 这么做的好处参见effective java
一书1
2
3
4
5
6
7
8
9
10
11
12
13var httpClient = HttpClient.newBuilder()
.authenticator(new BasicAuthenticator("user", "password"))
//.authenticator(Authenticator.getDefault()
.connectTimeout(Duration.ofSeconds(10))
.cookieHandler(CookieHandler.getDefault())
.executor(Executors.newFixedThreadPool(2))
.followRedirects(HttpClient.Redirect.NEVER)
.priority(1)
.proxy(ProxySelector.getDefault())
.sslContext(SSLContext.getDefault())
.sslParameters(new SSLParameters())
.version(HttpClient.Version.HTTP_2)
.build();or
1
var httpClient = HttpClient.newHttpClient();
equivalent
1
var httpCLient = HttpClient.newBuilder().build();
httpClient
类似String
设计模式是不变的,所以没有提供方法改变创建时候的参数.- 如果使用
http2
创建链接,但是服务端不支持,那么会自动降级成为http1.1
,如果没有指定,默认也是使用http2
excutor()
在使用异步请求时候使用,默认是使用线程池技术connectionTimeout()
默认没有超时时间priority()
优先级,范围[1-256],不在此范围会抛出异常connectTimeout()
链接超时设置,在设定的时间内没有连接上则抛出HttpConnectTimeoutException
executor()
用于异步任务执行,如果未指定,则会为每个HttpClient
实例创建一个.followRedirects()
当服务器返回30x
时,是否跳转,默认不跳转authenticator()
验证参数,Authenticator.getDefault()
获取当前验证规则,可以使用BasicAuthenticator
来传递用户名密码,也可以继承Authenticator
实现自己的验证规则.proxy()
是否使用代理.
创建
HttpRequest
1
2
3
4
5
6
7
8var httpRequset = HttpRequest.newBuilder(URI.create(""))
.header("Content-Type","application/json")
.header("token","faeaafwefeawgaer")
.timeout(Duration.of(10, ChronoUnit.SECONDS))
.expectContinue(true)
.POST(HttpRequest.BodyPublishers.ofString(""))
.version(HttpClient.Version.HTTP_2)
.build();uri()
可以在newBuidler()
中指定请求地址,也可以调用uri()
方法指定请求地址.两者效果是一样的header()
效果与setHeader()
相同,另有headers()
批量设置请求头, 请求头键值对必须严格按照RFC7230-section-3.2约定,否则抛出异常.timeout()
请求超时时间设置,超过设定时间未收到响应则抛出异常,如不限制会永远阻塞(等待)POST()
GET()
DELETE()
PUT()
请求方法,或者使用mehtod()
设置请求方法
使用前后端分离时候往往前端会发送一次options
请求来判断后端是否支持跨域,此时就可以使用method("OPTIONS",BodyPublishers.noBody())
BodyPublishers
用于构建BodyPublisher
的工具类,包含了一系列实用的构建请求体的方法,其中BodyPublishers
主要是调用RequestPublishers
来完成创建,RequestPublishers
中包含了很多BodyPublisher
接口的实现
HttpResponse
同步请求1
var httpResponse = httpClient.send(requset,BodyHandlers.ofString());
异步请求
1
2
3
4
5
6
7var httpResponse = httpClient.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofString())
.thenApply(stringHttpResponse -> {
System.out.println(stringHttpResponse.statusCode());
return stringHttpResponse;
})
.thenApply(HttpResponse::body)
.thenAccept(System.out::println);or 批量请求接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15var client = HttpClient.newHttpClient();
List<HttpRequest> requests = paths.stream()
.map(path -> "https://localhost:8443" + path)
.map(URI::create)
.map(uri -> HttpRequest.newBuilder(uri).build())
.collect(Collectors.toList());
CompletableFuture<?>[] responses = requests.stream()
.map(request -> client.sendAsync(request, BodyHandlers.ofString())
.thenApply(HttpResponse::body)
.exceptionally(e -> "Error: " + e.getMessage())
.thenAccept(System.out::println))
.toArray(CompletableFuture<?>[]::new);异步请求返回一个
CompletableFuture<HttpResponse<T>>
,当有响应返回时,该对象后续回调将会被调用.异步请求使用创建httpClient
时指定的executor
来执行异步请求.HttpResponse
为一个接口, 不能直接创建, 所有实例都是httpClient
请求返回, 接口提供方法如下:返回值 方法 描述 T body()
返回响应体 HttpHeaders headers()
返回响应投 int statusCode()
返回的状态码 HttpRequset request()
返回对应的请求体 URI uri()
返回请求地址 HttpClient.Version version()
返回http请求协议版本 BodyHandlers
用于构建BodyHandler
的工厂类.
进阶使用
JSON请求
发送请求时秩序指定
Content-Type
为application/json
, 然后将对象转换为json
字符串1
2
3
4
5
var httpRequest = HttpRequest.newBuilder(URI.create(""))
.header("content-type","application/json")
.GET()
.build();接受响应时,自定义
BodyHandler
将返回的json
字符串转换为对象1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28public class JsonHandler<T> implements HttpResponse.BodyHandler<T> {
private final Class<T> type;
private final Gson gson;
public JsonHandler(Class<T> type, Gson gson) {
this.type = type;
this.gson = gson;
}
public HttpResponse.BodySubscriber<T> apply(HttpResponse.ResponseInfo responseInfo) {
return HttpResponse.BodySubscribers.mapping(HttpResponse.BodySubscribers.ofByteArray(),bytes -> gson.fromJson(new String(bytes),this.type));
}
public static class JsonHandlers {
private JsonHandlers(){}
public static <T> JsonHandler<T> ofType(Class<T> type){
return of(new Gson(), type);
}
public static <T> JsonHandler<T> of(Gson gson, Class<T> type){
return new JsonHandler<T>( type,gson);
}
}
}使用
client
发送请求,并接收响应.1
2
3var client = HttpClient.newHttpClient(URI.create("Http://localhost:8080"));
var response = client.send(request, JsonHandler.JsonHandlers.ofType(UserBody.class));
var userBody = response.body();// or 使用异步响应
1
2
3
4
5var task = client.sendAsync(request, JsonHandler.JsonHandlers.ofType(UserBody.class))
.thenApply(HttpResponse::body)
.thenApply(UserBody::getName)
.thenAccept(System.out::println);
task.get();//测试方便输出结果.x-www-form-urlencoded
请求这种请求类型是
form
表单的默认请求类型,另一种就是可以上传文件的form-data
了,但是没有现成的类或者方法支持x-www-form-urlencoded
请求,不过该请求投类型很好分析将
form
表单里面的name
和value
用=
链接,在把他们用&
符号链接起来,如果包含空格替换为+
,如果有特殊符号,则转换为ASCII HEX
值;如果包含中文字符,则转成ASCII HEX
后在百分号编码.百分号编码: 汉字在
utf-8
字符集里面是占3个字节的,所以转换成16进制字符串就是占6个字节,每两个字节前面加一个百分号,就变成9个字节传递.如果是
GET
请求,那直接在url
后?
拼接.如果是
POST
请求, 那就把拼接好的字符串放在body
里面.简单点就是用现成的库
urlencoded
, 这种库应该是大部分语言都自带的.1
2
3
4
5
6
7
8
9
10
11
12public static HttpRequest.BodyPublisher ofXForm(Map<Object,Object> map){
var builder = new StringBuilder();
map.forEach((key, value) -> {
if (builder.length() > 0) {
builder.append("&");
}
builder.append(URLEncoder.encode(key.toString(), StandardCharsets.UTF_8));
builder.append("=");
builder.append(URLEncoder.encode(value.toString(), StandardCharsets.UTF_8));
});
return HttpRequest.BodyPublishers.ofString(builder.toString());
}
文件上传下载
下载
下载很简单直接,有现成的方法可以使用.
1
2
3
4var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create(url)).build();
var file = Paths.get("1.png");
var response = client.send(request,BodyHandlers.ofFile(file));该方法适合知道文件名称时使用.
or1
2
3
4var client = HttpClient.newHttpClient();
var request = HttpRequset.newBuilder(URI.create(url)).build();
var file = Paths.get("/usr/local/file");
var response = client.send(requset,BodyHandlers.ofFileDownload(file));ofFileDownload
属于比较常见的下载方式.上传
上传没有现成的方法,所以需要我们自定义一个
BodyPublishers.ofFile()
方法,然后请求头为mutipart/form-data
发送请求1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23public static HttpRequest.BodyPublisher ofFile(Map<Object,Object> data,String boundary) throws IOException {
var byteArrays = new ArrayList<byte[]>();
byte[] separator = ("--" + boundary + "\r\nContent-Disposition: form-data; name=")
.getBytes(StandardCharsets.UTF_8);
for (Map.Entry<Object, Object> entry : data.entrySet()) {
byteArrays.add(separator);
if (entry.getValue() instanceof Path) {
var path = (Path) entry.getValue();
String mimeType = Files.probeContentType(path);
byteArrays.add(("\"" + entry.getKey() + "\"; filename=\"" + path.getFileName()
+ "\"\r\nContent-Type: " + mimeType + "\r\n\r\n").getBytes(StandardCharsets.UTF_8));
byteArrays.add(Files.readAllBytes(path));
byteArrays.add("\r\n".getBytes(StandardCharsets.UTF_8));
}
else {
byteArrays.add(("\"" + entry.getKey() + "\"\r\n\r\n" + entry.getValue() + "\r\n")
.getBytes(StandardCharsets.UTF_8));
}
}
byteArrays.add(("--" + boundary + "--").getBytes(StandardCharsets.UTF_8));
return HttpRequest.BodyPublishers.ofByteArrays(byteArrays);
}1
2
3
4
5
6
7
8
9
10
11Map<Object,Object> data = new HashMap<>();
data.put("apikey", virusTotalApiKey);
data.put("file", localFile);
String boundary = new BigInteger(256, new Random()).toString();
request = HttpRequest.newBuilder()
.header("Content-Type", "multipart/form-data;boundary=" + boundary)
.POST(ofMimeMultipartData(data, boundary))
.uri(URI.create(url))
.build();
HttpResponse<String> response = client.send(request, BodyHandlers.ofString());
高阶使用
HTTP2 server push
WebSocket
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30WebSocket webSocket = HttpClient.newHttpClient().newWebSocketBuilder().buildAsync(new URI("ws://localhost:8081/platform/device/gps"), new WebSocket.Listener() {
public CompletionStage<?> onText(WebSocket webSocket,
CharSequence data, boolean last) {
System.out.println("onText: " + data);
return WebSocket.Listener.super.onText(webSocket, data, last);
}
public void onOpen(WebSocket webSocket) {
System.out.println("onOpen");
WebSocket.Listener.super.onOpen(webSocket);
}
public CompletionStage<?> onClose(WebSocket webSocket, int statusCode,
String reason) {
System.out.println("onClose: " + statusCode + " " + reason);
return WebSocket.Listener.super.onClose(webSocket, statusCode, reason);
}
}).join();
Gson gson = new Gson();
Message message = new Message();
message.setFrom("dbj");
message.setContent("client data send");
message.setTo("some one");
webSocket.sendText(gson.toJson(message),true);其中
super.OnXxxx()
为固定句式, 其实就是调用websocket.requset(1)
.固定调用.