企业 Java 应用程序定期访问外部 API、解析数据或在隔离的企业网络中工作——在每种情况下,迟早都会出现通过代理服务器路由流量的问题。在 Java 中设置代理看似简单的任务,直到你发现 RestTemplate、OkHttp 和 WebClient 的行为各不相同,而 JVM 的系统属性并不适用于所有情况。在本指南中——具体的代码示例、潜在问题的解释以及针对不同任务选择代理类型的建议。
Java 应用程序为什么需要代理
Java 仍然是企业开发的主要语言之一。Spring Boot 服务、微服务架构、批处理任务、ETL 管道——所有这些都定期向外部世界发出 HTTP 请求。在每种情况下,代理都解决了特定的问题:
- 强制使用代理网关的企业网络。 在许多公司,所有出站流量都通过企业代理(Squid、Zscaler、BlueCoat)。如果没有其设置,应用程序将无法访问互联网。
- 数据解析和收集。 收集外部网站数据(价格、报价、新闻)的服务使用代理来绕过速率限制和 IP 阻止。
- 地理测试。 QA 团队检查 API 或网站在不同地区的表现——代理可以模拟来自所需国家的请求。
- 绕过目标 API 的限制。 一些外部 API 对来自同一 IP 的请求数量有限制。代理轮换可以分散负载。
- 安全性和匿名化。 在访问外部服务时隐藏应用程序的真实 IP 是敏感集成的标准做法。
根据任务的不同,Java 中的代理设置方法有很大差异。JVM 的全局系统属性是最简单的选项,但它不适用于某些 HTTP 客户端。OkHttp 和 WebClient 需要明确的配置。我们将逐一分析每种情况。
通过 JVM 系统属性进行全局设置
将 Java 应用程序的所有 HTTP 流量通过代理的最快方法是使用 JVM 的系统属性。这适用于标准的 HttpURLConnection 和许多使用它的库。
可以通过几种方式设置代理:
1. 在启动时通过 JVM 参数:
java -Dhttp.proxyHost=proxy.example.com \
-Dhttp.proxyPort=8080 \
-Dhttps.proxyHost=proxy.example.com \
-Dhttps.proxyPort=8080 \
-Dhttp.nonProxyHosts="localhost|127.0.0.1|*.internal.corp" \
-jar myapp.jar
2. 通过应用程序代码(例如,在 main 方法或 @PostConstruct 中):
System.setProperty("http.proxyHost", "proxy.example.com");
System.setProperty("http.proxyPort", "8080");
System.setProperty("https.proxyHost", "proxy.example.com");
System.setProperty("https.proxyPort", "8080");
// 排除不应通过代理的主机
System.setProperty("http.nonProxyHosts", "localhost|127.0.0.1|*.internal.corp");
3. 通过 Spring Boot 中的 application.properties 文件(随后通过 @PostConstruct 应用):
# application.properties proxy.host=proxy.example.com proxy.port=8080 proxy.username=user proxy.password=secret
⚠️ 重要提示
JVM 的系统属性仅适用于 HttpURLConnection。OkHttp、Apache HttpClient 和 Reactor Netty(WebClient)会忽略这些属性——它们需要明确的配置,如下所述。
如果代理需要身份验证,请使用 Authenticator:
Authenticator.setDefault(new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
if (getRequestorType() == RequestorType.PROXY) {
return new PasswordAuthentication("username", "password".toCharArray());
}
return null;
}
});
Spring Boot 中的代理:RestTemplate 和 Apache HttpClient
RestTemplate 是 Spring 中的标准同步 HTTP 客户端。默认情况下,它使用 SimpleClientHttpRequestFactory,该工厂依赖于 HttpURLConnection 并读取 JVM 的系统属性。但为了更灵活的配置(超时、连接池、带身份验证的代理),建议使用 Apache HttpClient 作为后端。
Maven 依赖:
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>5.3.1</version>
</dependency>
通过 Apache HttpClient 5 配置带代理的 RestTemplate:
@Configuration
public class RestTemplateConfig {
@Value("${proxy.host}")
private String proxyHost;
@Value("${proxy.port}")
private int proxyPort;
@Value("${proxy.username:}")
private String proxyUsername;
@Value("${proxy.password:}")
private String proxyPassword;
@Bean
public RestTemplate restTemplate() {
HttpHost proxy = new HttpHost("http", proxyHost, proxyPort);
CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
if (!proxyUsername.isEmpty()) {
credentialsProvider.setCredentials(
new AuthScope(proxyHost, proxyPort),
new UsernamePasswordCredentials(proxyUsername, proxyPassword.toCharArray())
);
}
CloseableHttpClient httpClient = HttpClients.custom()
.setProxy(proxy)
.setDefaultCredentialsProvider(credentialsProvider)
.build();
HttpComponentsClientHttpRequestFactory factory =
new HttpComponentsClientHttpRequestFactory(httpClient);
factory.setConnectTimeout(5000);
factory.setReadTimeout(10000);
return new RestTemplate(factory);
}
}
这样的配置允许将代理参数提取到 application.properties 或环境变量中,这对于具有不同代理的生产/暂存/开发环境至关重要。
如果需要部分请求通过代理,部分请求直接发送,请创建两个不同名称的 RestTemplate Bean,并使用 @Qualifier 注入所需的。
Spring WebClient 中的代理(反应堆栈)
WebClient 是基于 Reactor Netty 的反应式 HTTP 客户端。它不会读取 JVM 的系统属性来设置代理——配置是在创建客户端时通过 ProxyProvider 完成的。
依赖项(包含在 spring-boot-starter-webflux 中):
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
带 HTTP 代理的 WebClient 配置:
@Configuration
public class WebClientConfig {
@Bean
public WebClient webClient() {
HttpClient httpClient = HttpClient.create()
.proxy(proxy -> proxy
.type(ProxyProvider.Proxy.HTTP)
.host("proxy.example.com")
.port(8080)
.username("user")
.password(user -> "password")
);
ClientHttpConnector connector =
new ReactorClientHttpConnector(httpClient);
return WebClient.builder()
.clientConnector(connector)
.build();
}
}
对于 WebClient 中的 SOCKS5 代理:
HttpClient httpClient = HttpClient.create()
.proxy(proxy -> proxy
.type(ProxyProvider.Proxy.SOCKS5)
.host("socks5.proxy.example.com")
.port(1080)
.username("user")
.password(user -> "password")
);
💡 配置建议
在反应式应用程序中,通常需要为不同的外部服务创建多个具有不同代理的 WebClient 实例。使用 WebClient.Builder 作为原型 Bean,并通过 .mutate() 重写设置。
在 OkHttp 中设置代理
OkHttp 是一个流行的 HTTP 库,许多 Java 和 Android 应用程序以及一些 Spring 集成(例如,Feign 与 OkHttp 后端)使用它。它有自己的代理设置机制,并且也不会读取 JVM 的系统属性。
OkHttp 中 HTTP/HTTPS 代理的基本设置:
import okhttp3.*;
import java.net.InetSocketAddress;
import java.net.Proxy;
// 创建代理
Proxy proxy = new Proxy(
Proxy.Type.HTTP,
new InetSocketAddress("proxy.example.com", 8080)
);
// 代理身份验证
Authenticator proxyAuthenticator = (route, response) -> {
String credential = Credentials.basic("username", "password");
return response.request().newBuilder()
.header("Proxy-Authorization", credential)
.build();
};
// 构建客户端
OkHttpClient client = new OkHttpClient.Builder()
.proxy(proxy)
.proxyAuthenticator(proxyAuthenticator)
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build();
// 请求示例
Request request = new Request.Builder()
.url("https://api.example.com/data")
.build();
try (Response response = client.newCall(request).execute()) {
System.out.println(response.body().string());
}
通过 Bean 将 OkHttp 集成到 Spring Boot 中:
@Configuration
@ConditionalOnProperty(name = "proxy.enabled", havingValue = "true")
public class OkHttpConfig {
@Value("${proxy.host}")
private String proxyHost;
@Value("${proxy.port}")
private int proxyPort;
@Bean
public OkHttpClient okHttpClient() {
Proxy proxy = new Proxy(
Proxy.Type.HTTP,
new InetSocketAddress(proxyHost, proxyPort)
);
return new OkHttpClient.Builder()
.proxy(proxy)
.connectionPool(new ConnectionPool(10, 5, TimeUnit.MINUTES))
.build();
}
}
请注意 @ConditionalOnProperty——这允许仅在需要的环境中通过配置启用代理,而无需更改代码。
如果您使用 OkHttp 与 住宅代理 进行数据解析,重要的是正确设置连接池。住宅代理通常具有比数据中心代理更高的延迟,因此请增加超时并减少池的大小,以免耗尽可用连接。
Java 中的 SOCKS5 代理:特点和示例
SOCKS5 是一种比 HTTP 代理更低级的协议。它在传输层工作,支持任何协议(HTTP、HTTPS、FTP 等)以及代理端的 DNS 解析。对于 Java 应用程序,当需要代理不仅仅是 HTTP,还包括其他 TCP 连接时,SOCKS5 特别有用。
通过 JVM 系统属性设置 SOCKS5:
System.setProperty("socksProxyHost", "socks5.proxy.example.com");
System.setProperty("socksProxyPort", "1080");
System.setProperty("java.net.socks.username", "user");
System.setProperty("java.net.socks.password", "password");
在 OkHttp 中使用 SOCKS5(通过 java.net.Proxy):
Proxy socks5Proxy = new Proxy(
Proxy.Type.SOCKS,
new InetSocketAddress("socks5.proxy.example.com", 1080)
);
OkHttpClient client = new OkHttpClient.Builder()
.proxy(socks5Proxy)
.build();
SOCKS5 和 DNS 的重要特点: 默认情况下,Java 在本地解析 DNS,并通过代理传递 IP 地址。这可能会泄露真实的 DNS 请求。要使 DNS 解析也通过代理进行,请使用以下 OkHttp 方法:
// 自定义 DNS,强制通过 SOCKS5 解析
OkHttpClient client = new OkHttpClient.Builder()
.proxy(socks5Proxy)
.dns(hostname -> {
// 返回未解析的主机——OkHttp 将其传递给代理
return Collections.singletonList(InetAddress.getByName(hostname));
})
.build();
何时选择 SOCKS5 与 HTTP 代理
| 标准 | HTTP 代理 | SOCKS5 代理 |
|---|---|---|
| 协议 | HTTP/HTTPS | 任何 TCP/UDP |
| 通过代理的 DNS | 取决于实现 | 支持 |
| 与 Java 的兼容性 | 原生 | 原生(java.net.Proxy.Type.SOCKS) |
| 使用场景 | 企业网关、网页解析 | 匿名化、非标准协议 |
Java 应用程序中的代理轮换
如果应用程序对一个资源发出大量请求,使用单一代理 IP 很快会导致被封锁。代理轮换——在不同 IP 地址之间切换——解决了这个问题。在 Java 中可以通过几种方式实现。
选项 1:通过轮换代理端点进行轮换
最简单的方法是使用具有单一端点的代理提供商,该端点在每次请求或通过指定间隔自动更改 IP。您只需指定一个主机和端口,轮换发生在提供商端。无需编写轮换代码。
选项 2:通过代理列表在应用程序端进行轮换
@Component
public class ProxyRotator {
private final List<ProxyConfig> proxies;
private final AtomicInteger counter = new AtomicInteger(0);
public ProxyRotator(@Value("${proxy.list}") List<String> proxyList) {
this.proxies = proxyList.stream()
.map(this::parseProxy)
.collect(Collectors.toList());
}
public OkHttpClient getClientWithNextProxy() {
int index = counter.getAndIncrement() % proxies.size();
ProxyConfig config = proxies.get(index);
Proxy proxy = new Proxy(
Proxy.Type.HTTP,
new InetSocketAddress(config.getHost(), config.getPort())
);
return new OkHttpClient.Builder()
.proxy(proxy)
.build();
}
private ProxyConfig parseProxy(String proxyStr) {
// 解析 "host:port" 或 "user:pass@host:port" 格式的字符串
String[] parts = proxyStr.split(":");
return new ProxyConfig(parts[0], Integer.parseInt(parts[1]));
}
}
选项 3:通过 ProxySelector 进行轮换
Java 提供了内置的 ProxySelector 机制,可以根据目标 URL 动态选择代理。这适用于 HttpURLConnection 和 Apache HttpClient:
ProxySelector.setDefault(new ProxySelector() {
private final List<Proxy> proxyPool = Arrays.asList(
new Proxy(Proxy.Type.HTTP, new InetSocketAddress("proxy1.example.com", 8080)),
new Proxy(Proxy.Type.HTTP, new InetSocketAddress("proxy2.example.com", 8080)),
new Proxy(Proxy.Type.HTTP, new InetSocketAddress("proxy3.example.com", 8080))
);
private final AtomicInteger idx = new AtomicInteger(0);
@Override
public List<Proxy> select(URI uri) {
// 可以根据域名添加选择逻辑
int i = idx.getAndIncrement() % proxyPool.size();
return Collections.singletonList(proxyPool.get(i));
}
@Override
public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
// 处理不可用代理的逻辑
log.warn("Proxy failed: {}", sa);
}
});
Java 任务选择哪种类型的代理
选择代理类型取决于具体任务。对于企业 Java 应用程序,通常考虑三种主要类型:
| 代理类型 | 速度 | 匿名性 | 最佳用途 |
|---|---|---|---|
| 数据中心代理 | 高 | 中 | 解析公开数据、API 测试、高负载 |
| 住宅代理 | 中 | 高 | 解析受保护的网站、地理测试、绕过封锁 |
| 移动代理 | 中 | 最高 | 与移动 API 的集成、具有严格反欺诈保护的服务 |
任务类型的实用建议:
- 企业网关(Squid、Zscaler): 代理类型由 IT 部门确定。使用带身份验证的 HTTP 代理通过 Apache HttpClient 或 JVM 系统属性。
- 解析公共数据(新闻、报价、开放 API): 数据中心代理是速度与成本的最佳选择。
- 解析电子商务(Wildberries、Ozon、Amazon): 带轮换的住宅代理。这些平台积极阻止数据中心 IP。
- 地理测试(检查本地化、各地区价格): 带国家/城市选择的住宅代理。
- 与移动服务和应用程序的集成: 移动代理——来自真实移动网络的 IP 最能模拟真实用户。
Java 中设置代理时的常见错误及其解决方法
即使是经验丰富的开发人员在设置 Java 代理时也会遇到相同的问题。让我们来看看最常见的错误。
1. 代理已设置,但流量仍然直接发送
原因: 您已设置 JVM 的系统属性,但使用的是 OkHttp 或 Reactor Netty(WebClient),它们会忽略这些属性。
解决方案: 在特定 HTTP 客户端的配置中明确设置代理,如上面的部分所示。
2. javax.net.ssl.SSLHandshakeException 通过代理的 HTTPS
原因: 企业代理进行 SSL 检查(MITM),替换证书。Java 不信任企业 CA。
解决方案: 将企业证书导入 Java 信任库:
keytool -import -trustcacerts \ -alias corporate-ca \ -file corporate-ca.crt \ -keystore $JAVA_HOME/lib/security/cacerts \ -storepass changeit
3. java.net.ConnectException:连接被拒绝
原因: 代理的主机或端口不正确;代理服务器不可用;防火墙阻止连接。
诊断:
# 检查代理的可用性 curl -x http://proxy.example.com:8080 https://httpbin.org/ip # 或通过 telnet telnet proxy.example.com 8080
4. 代理对 HTTP 有效,但对 HTTPS 无效
原因: 仅设置了 http.proxyHost/Port 属性,而没有 https.proxyHost/Port。
解决方案: 设置两组属性。通过 HTTP 代理的 HTTPS 使用 CONNECT 隧道方法——确保代理支持此功能。
5. 407 代理身份验证所需
原因: 代理需要身份验证,但未设置。
解决方案: 为 JVM 的系统属性添加 Authenticator,或为 Apache HttpClient / proxyAuthenticator 进行设置(上面的示例)。
6. 代理轮换时内存泄漏
原因: 每次请求都会创建新的 OkHttpClient 实例。每个客户端都保持自己的线程和连接池。
解决方案: 复用客户端。创建多个客户端的池(每个代理一个),并轮换它们,而不是创建新的。
7. 启用调试日志以进行诊断
# application.properties — 启用代理诊断日志 logging.level.org.apache.http=DEBUG logging.level.org.apache.http.wire=DEBUG logging.level.reactor.netty.http.client=DEBUG # 对于 JVM — 系统属性 -Djava.net.debug=all
结论
在 Java 应用程序中设置代理需要了解底层使用的 HTTP 客户端。JVM 的系统属性是 HttpURLConnection 的快速入门,但 OkHttp、WebClient 和 Apache HttpClient 需要明确的配置。对于企业应用程序,建议将代理参数提取到 application.properties 中,并使用 Spring 配置文件来适应不同的环境。代理轮换最好委托给具有轮换端点的提供商——这比自己实现更简单、更可靠。
如果您的 Java 应用程序正在解析受保护平台的数据或在外部请求时需要高度匿名性,建议考虑使用 住宅代理——它们具有真实家庭用户的 IP,这大大降低了反欺诈系统的封锁概率。对于高负载的公开数据解析,最佳选择是 数据中心代理——它们在较低的流量成本下提供最大速度。
```