# Java HttpClient 代理接入示例

使用 JDK HttpClient、ProxySelector、代理认证和超时完成 HTTPS 请求。

JDK 11 及以上可直接使用标准库 `java.net.http.HttpClient`，无需为基础 HTTP 代理访问增加第三方客户端。

## 前置条件

- 运行环境使用 JDK 11 或更高版本。
- 通过受控环境注入 `PROXY_HOST`、`PROXY_PORT`、`PROXY_USERNAME` 和 `PROXY_PASSWORD`。
- 代理密码只保留在进程环境或密钥服务中。

## 完整示例

```java
import java.net.Authenticator;
import java.net.InetSocketAddress;
import java.net.PasswordAuthentication;
import java.net.ProxySelector;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;

public final class ProxyExample {
    /**
     * 使用 JDK HttpClient 通过账密代理请求公开出口检测地址。
     */
    public static void main(String[] args) throws Exception {
        String proxyHost = System.getenv("PROXY_HOST");
        int proxyPort = Integer.parseInt(System.getenv("PROXY_PORT"));
        String proxyUsername = System.getenv("PROXY_USERNAME");
        char[] proxyPassword = System.getenv("PROXY_PASSWORD").toCharArray();

        HttpClient client = HttpClient.newBuilder()
            .connectTimeout(Duration.ofSeconds(10))
            .proxy(ProxySelector.of(new InetSocketAddress(proxyHost, proxyPort)))
            .authenticator(new Authenticator() {
                /** 仅向代理服务器提供认证信息，避免响应目标站认证挑战。 */
                @Override
                protected PasswordAuthentication getPasswordAuthentication() {
                    if (getRequestorType() != RequestorType.PROXY) return null;
                    return new PasswordAuthentication(proxyUsername, proxyPassword);
                }
            })
            .build();

        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://<public-test-host>/ip"))
            .timeout(Duration.ofSeconds(30))
            .GET()
            .build();
        HttpResponse<String> response = client.send(
            request,
            HttpResponse.BodyHandlers.ofString()
        );

        if (response.statusCode() >= 400) {
            throw new IllegalStateException("HTTP " + response.statusCode());
        }
        System.out.println(response.body());
    }
}
```

## 预期结果

- 客户端先连接代理，再为 HTTPS 目标建立隧道。
- 响应正文显示代理出口，状态码小于 400。
- 长生命周期服务应复用 `HttpClient`，保留连接池收益。

## 错误处理

- 407 表示代理认证未通过，检查用户名、密码和权益。
- `HttpConnectTimeoutException` 表示连接阶段超过 10 秒。
- 目标状态码和代理连接异常必须分开记录。

## 安全提示

不要手工写入 `Proxy-Authorization`，也不要在异常日志中输出环境变量。代理身份变化时创建新的客户端，避免旧连接继续携带原认证状态。

html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}
