JDK11新特性:HttpClient,取代URLConnection神器

一、简介

java的Http客户端库,不说五花八门吧也算是百花齐放了。归根结底的原因就是JDK自带的http工具URLConnection太难用了!

JDK11之后,JDk推出了一个全新的HttpClient库,基于了响应式流(Reactive Stream)模型,使用了JAVA 9引入的Flow api,并运用completableFuture使调用流程异步化(在IO层面实现非阻塞化-NIO),带来了性能的提升和焕然一新的使用体验。

HttpClient 支持同步和异步两种请求发送模式。
同步请求方式使用的是 send 方法,参数是 HttpRequest 和 BodyHandler,返回值是 HttpResponse<T>
异步请求方式使用的是sendAsync方法,参数同样是 HttpRequest 和 BodyHandler,返回值是 CompletableFuture<HttpResponse<T>>

注意!
Http连接池不支持手动配置,默认是无限复用的
重试次数不支持手动配置
不指定Http客户端或请求的版本,会默认使用Http2模式进行连接,受挫后会进行降级
请求的同步发送模式(send)实际上会后台另开线程

二、使用

1、发送get、post请求

HttpClient client = HttpClient.newBuilder()
    .version(Version.HTTP_1_1)	//可以手动指定客户端的版本,如果不指定,那么默认是Http2
    .followRedirects(Redirect.NORMAL)	//设置重定向策略
    .connectTimeout(Duration.ofSeconds(20))	//连接超时时间
    .proxy(ProxySelector.of(new InetSocketAddress("proxy.example.com", 80)))	//代理地址设置
    .authenticator(Authenticator.getDefault()) 
    //.executor(Executors.newFixedThreadPoolExecutor(8))  //可手动配置线程池
    .build();   

HttpRequest request = HttpRequest.newBuilder()       
    .uri(URI.create("https://foo.com/"))    //设置url地址
    .GET()
    .build();
HttpResponse<String> response = client.send(request, BodyHandlers.ofString());  	//同步发送
System.out.println(response.statusCode()); 	//打印响应状态码
System.out.println(response.body());  


HttpRequest request = HttpRequest.newBuilder()
       .uri(URI.create("https://foo.com/")) 
       .timeout(Duration.ofMinutes(2))	//设置连接超时时间
       .header("Content-Type", "application/json")	
       .POST(BodyPublishers.ofFile(Paths.get("file.json")))    //设置请求体来源
       .build();   
client.sendAsync(request, BodyHandlers.ofString()) 		//异步发送
    .thenApply(HttpResponse::body) 	//发送结束打印响应体
    .thenAccept(System.out::println);  

2、会话保持

//使用默认的CookieManager,并且接受所有第三方Cookie
cookieManager = new CookieManager();
cookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ALL);
httpClient = HttpClient.newBuilder()
        .connectTimeout(Duration.ofMillis(5000))
        .cookieHandler(cookieManager)//注意在此步骤送送入
        .followRedirects(HttpClient.Redirect.NORMAL)
        .build();


3、发送multipart/form-data数据

遗憾的是,JDK 11 HttpClient对这类数据支持不太友好。

private val httpClient: HttpClient = HttpClient.newHttpClient()

fun main() {
    val file = File(
        LocalTest::class.java.classLoader.getResource("my_pic_from_resources.jpg").file
    )
    val token = "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11"

    val data: MutableMap<String, Any> = LinkedHashMap()
    data["chat_id"] = "123456789"
    data["photo"] = file
    val boundary: String = BigInteger(35, Random()).toString()

    val request = HttpRequest.newBuilder()
        .uri(URI.create("https://api.telegram.org/bot$token/sendPhoto"))
        .postMultipartFormData(boundary, data)
        .build()

    val response = httpClient.send(request, HttpResponse.BodyHandlers.ofString())
    println(response)
}

fun HttpRequest.Builder.postMultipartFormData(boundary: String, data: Map<String, Any>): HttpRequest.Builder {
    val byteArrays = ArrayList<ByteArray>()
    val separator = "--$boundary\r\nContent-Disposition: form-data; name=".toByteArray(StandardCharsets.UTF_8)

    for (entry in data.entries) {
        byteArrays.add(separator)
        when(entry.value) {
            is File -> {
                val file = entry.value as File
                val path = Path.of(file.toURI())
                val mimeType = Files.probeContentType(path)
                byteArrays.add("\"${entry.key}\"; filename=\"${path.fileName}\"\r\nContent-Type: $mimeType\r\n\r\n".toByteArray(StandardCharsets.UTF_8))
                byteArrays.add(Files.readAllBytes(path))
                byteArrays.add("\r\n".toByteArray(StandardCharsets.UTF_8))
            }
            else -> byteArrays.add("\"${entry.key}\"\r\n\r\n${entry.value}\r\n".toByteArray(StandardCharsets.UTF_8))
        }
    }
    byteArrays.add("--$boundary--".toByteArray(StandardCharsets.UTF_8))

    this.header("Content-Type", "multipart/form-data;boundary=$boundary")
        .POST(HttpRequest.BodyPublishers.ofByteArrays(byteArrays))
    return this
}


var body = new MultipartFormData()
            .add(StringPart("name", "Hello,")
            .add(StringPart("value", "World!")
            .addFile("f", Path.of("index.html"), "text/html")
            .addFile("cpuinfo", Path.of("/proc/cpuinfo"), "text/html");

var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("http://localhost:8080/"))
    .header("Content-Type", body.contentType())
    .POST(body)
    .build();
var response = client.send(request, BodyHandlers.ofLines());
response.body().forEach(line -> System.out.println(line));

或者:

        HttpClient client = HttpClient.newHttpClient();
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("http://127.0.0.1/user"))
                .timeout(Duration.ofMinutes(2))
                .header("Content-Type", "application/x-www-form-urlencoded;charset=utf-8")
                .POST(HttpRequest.BodyPublishers.ofString("id=1&userName=赵云"))
                .build();
        String body = client.send(request, HttpResponse.BodyHandlers.ofString()).body();
        System.out.println("阻塞请求结果:" + body);

4、websocket长连接

使用HttpClient创建WebSocket实时通信

        HttpClient client = HttpClient.newHttpClient();
        WebSocket webSocket = client.newWebSocketBuilder()
                .buildAsync(URI.create("ws://localhost:8080/hello"), new WebSocket.Listener() {
                    @Override
                    public CompletionStage<?> onText(WebSocket webSocket, CharSequence data, boolean last) {
                        webSocket.request(1);
                        return CompletableFuture.completedFuture(data).thenAccept(System.out::println);
                    }
                }).join();
        webSocket.sendText("hello ", false);
        webSocket.sendText("world ", true);
        TimeUnit.SECONDS.sleep(10);
        webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "ok").join();

三、源码分析

1、HttpClient客户端构建

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())
 
//创建完成 
// 这里使用了典型的建造者模式(Builder Pattern)。使用建造者模式的好处是:Httpclient相对复杂的构建过程被隐藏了起来,使用者无需知道具体的构建细节。
.build();

最终构建的逻辑是在build方法中执行的,最后的构建过程交给了HttpClientImpl。
在这里插入图片描述

/**
 * Client implementation. Contains all configuration information and also
 * the selector manager thread which allows async events to be registered
 * and delivered when they occur. See AsyncEvent.
 */
final class HttpClientImpl extends HttpClient implements Trackable {
    
    static final AtomicLong CLIENT_IDS = new AtomicLong();

    //此处列出大部分成员变量
    
    private final CookieHandler cookieHandler;
    private final Duration connectTimeout;
    private final Redirect followRedirects;
    private final ProxySelector userProxySelector;
    private final ProxySelector proxySelector;
    private final Authenticator authenticator;
    private final Version version;
    private final ConnectionPool connections;
    private final DelegatingExecutor delegatingExecutor;
    private final boolean isDefaultExecutor;
    // Security parameters
    private final SSLContext sslContext;
    private final SSLParameters sslParams;
    private final SelectorManager selmgr;
    private final FilterFactory filters;
    //HttpClient2客户端
    private final Http2ClientImpl client2;
    private final long id;
    private final String dbgTag;
    
    private final SSLDirectBufferSupplier sslBufferSupplier
            = new SSLDirectBufferSupplier(this);
    //对HttpClient外观实现类的弱引用
    private final WeakReference<HttpClientFacade> facadeRef;
	
    //对待处理的请求的计数
    private final AtomicLong pendingOperationCount = new AtomicLong();
    private final AtomicLong pendingWebSocketCount = new AtomicLong();
    private final AtomicLong pendingHttpRequestCount = new AtomicLong();
    private final AtomicLong pendingHttp2StreamCount = new AtomicLong();

    /** 过期时间 */
    private final TreeSet<TimeoutEvent> timeouts;
/**
     * This is a bit tricky:
     * 1. an HttpClientFacade has a final HttpClientImpl field.
     * 2. an HttpClientImpl has a final WeakReference<HttpClientFacade> field,
     *    where the referent is the facade created for that instance.
     * 3. We cannot just create the HttpClientFacade in the HttpClientImpl
     *    constructor, because it would be only weakly referenced and could
     *    be GC'ed before we can return it.
     * The solution is to use an instance of SingleFacadeFactory which will
     * allow the caller of new HttpClientImpl(...) to retrieve the facade
     * after the HttpClientImpl has been created.
     */
    //外观工厂
    private static final class SingleFacadeFactory {
        HttpClientFacade facade;
        HttpClientFacade createFacade(HttpClientImpl impl) {
            assert facade == null;
            return (facade = new HttpClientFacade(impl));
        }
    }

  //我们要分析的方法
    static HttpClientFacade create(HttpClientBuilderImpl builder) {
        //这是个Factory?这是在做什么呢?
        //其实,这里是HttpClient的外观代理实现类HttpClientFacade的构建工厂。
        SingleFacadeFactory facadeFactory = new SingleFacadeFactory();
        //构建方法的重点:实例化HttpClientImpl。稍后详细分析
        HttpClientImpl impl = new HttpClientImpl(builder, facadeFactory);
        //启动NIO选择子管理者守护线程,接收并响应I/O事件
        impl.start();
        assert facadeFactory.facade != null;
        assert impl.facadeRef.get() == facadeFactory.facade;
        //返回外观实现类
        return facadeFactory.facade;
    }
    
    //HttpClientImpl的初始化构造方法
//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;
    }
    
}

调用build()方法返回的是一个HttpClientFacade的外观实现类。HttpClientFacade是什么?它和HttpClientImpl的关系是什么?SingleFacadeFactory又是什么用途呢?因此,我们通过UML类图来说明。
在这里插入图片描述

2、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();

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

秃了也弱了。

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值