<mark>此插件，主要社区贡献人（Will）</mark>


```xml
<dependency>
    <groupId>org.noear</groupId>
    <artifactId>solon-net-httputils</artifactId>
</dependency>
```


### 1、描述

网络扩展插件。提供基础的 http 调用。默认基于 HttpURLConnection 适配，当有 okhttp 引入时自动切换为 okhttp 适配。 

* HttpURLConnection 适配时，为 40Kb 左右
* OkHttp 适配时，为 3.2 Mb 左右（项目里有 OkHttp 依赖时）


v3.0 后，同时做为 solon-test 和 nami-channel-http 的内部工具。




### 2、基本操作


* HEAD 请求

```java
int code = HttpUtils.http("http://localhost:8080/hello").head();
```

* GET 请求

```java
String body = HttpUtils.http("http://localhost:8080/hello").get();
```

```java
//for Bean
Book book = HttpUtils.http("http://localhost:8080/book?bookId=1")
                     .getAs(Book.class);
```

* POST 请求

```java
//x-www-form-urlencoded
String body = HttpUtils.http("http://localhost:8080/hello")
                       .data("name","world")
                       .post();

//form-data
String body = HttpUtils.http("http://localhost:8080/hello")
                       .data("name","world")
                       .post(true); // useMultipart
                       
//form-data :: upload-file
String body = HttpUtils.http("http://localhost:8080/hello")
                       .data("name", new File("/data/demo.jpg"))
                       .post(true); // useMultipart
                       
//body-json
String body = HttpUtils.http("http://localhost:8080/hello")
                       .bodyOfJson("{\"name\":\"world\"}")
                       .post();
```

```java
//for Bean
Result body = HttpUtils.http("http://localhost:8080/book")
                       .bodyOfBean(book)
                       .postAs(Result.class);
                       
                       
//for Bean generic type
Result<User> body = HttpUtils.http("http://localhost:8080/book")
                       .bodyOfBean(book)
                       .postAs(new Result<User>(){}.getClass()); //通过临时类构建泛型（或别的方式）
```

* PUT 请求

```java
String body = HttpUtils.http("http://localhost:8080/hello")
                       .data("name","world")
                       .put();
```

```java
//for Bean
Result body = HttpUtils.http("http://localhost:8080/book")
                       .bodyOfBean(book)
                       .putAs(Result.class);
```


* PATCH 请求

```java
String body = HttpUtils.http("http://localhost:8080/hello")
                       .data("name","world")
                       .patch();
```

```java
//for Bean
Result body = HttpUtils.http("http://localhost:8080/book")
                       .bodyOfBean(book)
                       .patchAs(Result.class);
```

* DELETE 请求

```java
String body = HttpUtils.http("http://localhost:8080/hello")
                       .data("name","world")
                       .delete();
```


```java
//for Bean
Result body = HttpUtils.http("http://localhost:8080/book")
                       .bodyOfBean(book)
                       .deleteAs(Result.class);
```

### 3、高级操作

获取响应（用完要关闭）

```java
try(HttpResponse resp = HttpUtils.http("http://localhost:8080/hello").data("name","world").exec("POST")) {
    int code = resp.code();
    String head = resp.header("Demo-Header");
    String body = resp.bodyAsString();
}
```

配置序列化器。默认为 json，改为 fury；或者自己定义。

```java
FuryBytesSerializer serializer = new FuryBytesSerializer();

Result body = HttpUtils.http("http://localhost:8080/book")
                       .serializer(serializer)
                       .bodyOfBean(book)
                       .postAs(Result.class);
```

定制扩展（统一添加头信息等...）

```java
//注解模式
@Component
public class HttpExtensionImpl implements HttpExtension {
    @Override
    public void onInit(HttpUtils httpUtils, String url) {
        httpUtils.header("TOKEN","xxxx");
    }
}

//手动模式
HttpConfiguration.addExtension((httpUtils, url)->{
    httpUtils.header("TOKEN","xxxx");
});
```

指定适配工厂（默认情况：当有 okhttp 自动切换为 okhttp 适配，否则为 jdkhttp）：

```java
//指定 jdkhttp 的适配
HttpConfiguration.setFactory(new JdkHttpUtilsFactory());

//指定 okhttp 的适配
HttpConfiguration.setFactory(new OkHttpUtilsFactory());

//也可添加自己的适配实现
```

### 4、SSL 配置（https）

SSL 配置提供者接口（HttpSslSupplier），是提供 ssl 配置支持的接口


| 内置实现 | 描述 | 备注 |
| -------- | -------- | -------- |
| HttpSslSupplierDefault     | 默认实现，对常规 ssl 提供支持     | 某些太旧的或私有的证书，可能请求失败     |
| HttpSslSupplierAny         | 支持任意证书，不做校验     | 如果 url 是不确定的，可能会有安全问题     |


使用示例：

```java
String html = HttpUtils.http("https://solon.noear.org")
                       .ssl(HttpSslSupplierAny.getInstance())
                       .get();
```

更多配置，请参考后面的接口。


### 5、其它操作

基于服务名的调用示例：（内部是基于注册与发布服务）

```java
public class App {
    public static void maing(String[] args) {
        Solon.start(App.class, args);

        //通过服务名进行http请求
        HttpUtils.http("HelloService","/hello").get();
        HttpUtils.http("HelloService","/hello").data("name", "world").put();
        HttpUtils.http("HelloService","/hello").bodyOfJson("{\"name\":\"world\"}").post();
    }
}
```



顺带放了一个预热工具，让自己可以请求自己。从而达到简单预热效果：

```java
public class App {
    public static void maing(String[] args) {
        Solon.start(App.class, args);

        //用http请求自己进行预热
        PreheatUtils.preheat("/healthz");

        //用bean预热
        HelloService service = Solon.context().getBean(HelloService.class);
        service.hello();
    }
}
```

### 6、Http 流式（文本流）获取操作（基于 solon-rx 接口返回）

* 获取以“行”为单位的文本流（比如 ndjson ）

```java
HttpUtils.http("http://localhost:8080/ndjson")
        .execAsTextStream("GET")
        .subscribe(new SimpleSubscriber<String>().doOnNext(line -> {
            System.out.println(line);
        }));
```

也可用于逐行读取网页

```java
@Test
public void case1() throws Exception {
    CountDownLatch latch = new CountDownLatch(1);
    HttpUtils.http("https://solon.noear.org/")
            .execAsTextStream("GET")
            .subscribe(new SimpleSubscriber<String>().doOnNext(line -> {
                System.out.println(line);
            }).doOnComplete(() -> {
                latch.countDown();
            }));

    latch.await();
}
```

* 获取以“SSE”（Server Sent Event）为单位的文本流

```java
Flux<ServerSentEvent> publisher = HttpUtils.http("http://localhost:8080/sse")
                .execAsEventStream("GET");
```


### 7、接口说明

```java
public interface HttpUtils {
    static final Logger log = LoggerFactory.getLogger(HttpUtils.class);

    /**
     * 创建
     */
    static HttpUtils http(String service, String path) {
        String url = LoadBalanceUtils.getServer(null, service) + path;
        return http(url);
    }

    /**
     * 创建
     */
    static HttpUtils http(String group, String service, String path) {
        String url = LoadBalanceUtils.getServer(group, service) + path;
        return http(url);
    }

    /**
     * 创建
     */
    static HttpUtils http(String url) {
        return HttpConfiguration.getFactory().http(url);
    }

    /**
     * 发起地址
     * 
     * @since 3.9.5
     */
    String url();

    /**
     * 配置序列化器
     */
    HttpUtils serializer(Serializer serializer);

    /**
     * 获取序列化器
     */
    Serializer serializer();

    /**
     * 启用打印（专为 tester 服务）
     */
    HttpUtils enablePrintln(boolean enable);

    /**
     * 代理配置
     */
    HttpUtils proxy(Proxy proxy);

    /**
     * ssl
     */
    HttpUtils ssl(HttpSslSupplier sslProvider);

    /**
     * 代理配置
     */
    default HttpUtils proxy(String host, int port) {
        return proxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress(host, port)));
    }

    /**
     * 超时配置
     */
    default HttpUtils timeout(int timeoutSeconds) {
        return timeout(HttpTimeout.of(timeoutSeconds));
    }

    /**
     * 超时配置
     */
    default HttpUtils timeout(int connectTimeoutSeconds, int writeTimeoutSeconds, int readTimeoutSeconds) {
        return timeout(HttpTimeout.of(connectTimeoutSeconds, writeTimeoutSeconds, readTimeoutSeconds));
    }

    /**
     * 超时配置
     */
    HttpUtils timeout(HttpTimeout timeout);

    /**
     * 是否多部分配置
     */
    HttpUtils multipart(boolean multipart);

    /**
     * 用户代理配置
     */
    HttpUtils userAgent(String ua);

    /**
     * 编码配置
     */
    HttpUtils charset(String charset);

    /**
     * 头配置
     */
    HttpUtils headers(Map headers);

    /**
     * 头配置
     */
    HttpUtils headers(Iterable<KeyValues<String>> headers);

    /**
     * 头配置（替换）
     */
    HttpUtils header(String name, String value);

    /**
     * 头配置（添加）
     */
    HttpUtils headerAdd(String name, String value);


    /**
     * Content-Type 头配置
     */
    default HttpUtils contentType(String contentType) {
        return header("Content-Type", contentType);
    }

    /**
     * Accept 头配置
     */
    default HttpUtils accept(String accept) {
        return header("Accept", accept);
    }


    /**
     * 小饼配置
     */
    HttpUtils cookies(Map cookies);

    /**
     * 小饼配置
     */
    HttpUtils cookies(Iterable<KeyValues<String>> cookies);

    /**
     * 小饼配置（替换）
     */
    HttpUtils cookie(String name, String value);

    /**
     * 小饼配置（添加）
     */
    HttpUtils cookieAdd(String name, String value);

    /**
     * 参数配置
     */
    HttpUtils data(Map data);

    /**
     * 参数配置
     */
    HttpUtils data(Iterable<KeyValues<String>> data);

    /**
     * 参数配置（替换）
     */
    HttpUtils data(String name, String value);

    /**
     * 参数配置
     */
    HttpUtils data(String name, String filename, InputStream inputStream, String contentType);

    /**
     * 参数配置
     */
    HttpUtils data(String name, String filename, File file);

    /**
     * 参数配置
     */
    default HttpUtils data(String name, File file) {
        return data(name, file.getName(), file);
    }

    /**
     * 主体配置
     */
    default HttpUtils bodyOfTxt(String txt) {
        return body(txt, MimeType.TEXT_PLAIN_VALUE);
    }

    /**
     * 主体配置
     */
    default HttpUtils bodyOfJson(String txt) {
        return body(txt, MimeType.APPLICATION_JSON_VALUE);
    }

    /**
     * 主体配置（由序列化器决定内容类型）
     */
    HttpUtils bodyOfBean(Object obj) throws HttpException;

    /**
     * 主体配置
     */
    HttpUtils body(String txt, String contentType);

    /**
     * 主体配置
     */
    HttpUtils body(byte[] bytes, String contentType);

    /**
     * 主体配置
     */
    default HttpUtils body(byte[] bytes) {
        return body(bytes, null);
    }

    /**
     * 主体配置
     */
    HttpUtils body(InputStream raw, String contentType);

    /**
     * 主体配置
     */
    default HttpUtils body(InputStream raw) {
        return body(raw, null);
    }


    /**
     * get 请求并返回 body
     */
    String get() throws HttpException;

    /**
     * get 请求并返回 body
     */
    <T> T getAs(Type type) throws HttpException;

    /**
     * post 请求并返回 body
     */
    String post() throws HttpException;

    /**
     * post 请求并返回 body
     */
    <T> T postAs(Type type) throws HttpException;

    /**
     * post 请求并返回 body
     */
    default String post(boolean useMultipart) throws HttpException {
        if (useMultipart) {
            multipart(true);
        }

        return post();
    }

    /**
     * post 请求并返回 body
     */
    default <T> T postAs(Type type, boolean useMultipart) throws HttpException {
        if (useMultipart) {
            multipart(true);
        }

        return postAs(type);
    }

    /**
     * put 请求并返回 body
     */
    String put() throws HttpException;

    /**
     * put 请求并返回 body
     */
    <T> T putAs(Type type) throws HttpException;

    /**
     * patch 请求并返回 body
     */
    String patch() throws HttpException;

    /**
     * patch 请求并返回 body
     */
    <T> T patchAs(Type type) throws HttpException;

    /**
     * delete 请求并返回 body
     */
    String delete() throws HttpException;

    /**
     * delete 请求并返回 body
     */
    <T> T deleteAs(Type type) throws HttpException;


    /**
     * options 请求并返回 body
     */
    String options() throws HttpException;

    /**
     * head 请求并返回 code
     */
    int head() throws HttpException;

    //////

    /**
     * 执行请求并返回响应主体
     */
    String execAsBody(String method) throws HttpException;

    /**
     * 执行请求并返回响应主体
     */
    <T> T execAsBody(String method, Type type) throws HttpException;

    /**
     * 执行请求并返回代码
     */
    int execAsCode(String method) throws HttpException;

    /**
     * 执行请求并返回文本行流
     */
    Flux<String> execAsLineStream(String method);

    /**
     * 执行请求并返回文本流
     *
     * @deprecated 3.1 {@link #execAsLineStream(String)}
     */
    @Deprecated
    default Flux<String> execAsTextStream(String method) {
        return execAsLineStream(method);
    }


    /**
     * 执行请求并返回服务端推送事件流
     */
    Flux<ServerSentEvent> execAsSseStream(String method);

    /**
     * 执行请求并返回服务端推送事件流
     *
     * @deprecated 3.1 {@link #execAsSseStream(String)}
     */
    @Deprecated
    default Flux<ServerSentEvent> execAsEventStream(String method) {
        return execAsSseStream(method);
    }

    /**
     * 执行请求并返回响应
     */
    HttpResponse exec(String method) throws HttpException;

    /**
     * 异步执行请求
     */
    CompletableFuture<HttpResponse> execAsync(String method);

    /**
     * 填充自己
     */
    default HttpUtils fill(Consumer<HttpUtils> consumer) {
        consumer.accept(this);
        return this;
    }


    /////////////

    /**
     * url 编码
     */
    static String urlEncode(String s) throws IOException {
        return urlEncode(s, null);
    }

    /**
     * url 编码
     */
    static String urlEncode(String s, String charset) throws UnsupportedEncodingException {
        if (charset == null) {
            return URLEncoder.encode(s, Solon.encoding());
        } else {
            return URLEncoder.encode(s, charset);
        }
    }

    /**
     * map 转为 queryString
     */
    static CharSequence toQueryString(Map<?, ?> map) throws IOException {
        return toQueryString(map, null);
    }

    /**
     * map 转为 queryString
     */
    static CharSequence toQueryString(Map<?, ?> map, String charset) throws IOException {
        Assert.notNull(map, "map");

        StringBuilder buf = new StringBuilder();
        for (Map.Entry<?, ?> entry : map.entrySet()) {
            //过滤 null
            if (entry.getValue() != null) {
                if (buf.length() > 0) {
                    buf.append('&');
                }

                buf.append(urlEncode(entry.getKey().toString(), charset))
                        .append('=')
                        .append(urlEncode(entry.getValue().toString(), charset));
            }
        }

        return buf.toString();
    }
}
```
