先搞清楚:代理配在哪一层
Java里给请求挂代理,说穿了就两条路。一条是JVM全局参数,启动时用 -D 参数把代理地址交给虚拟机,之后所有HTTP请求默认都从它走;另一条是代码级配置,在构造客户端时把Proxy对象传进去,想给哪个请求配就给哪个请求配。
两种方式没有绝对的好坏。如果整个应用的所有出站请求都要走代理,全局参数省事,一行启动命令搞定;如果只有采集任务走代理、定时任务和内部服务调用直连,那就老老实实在代码里配,别让全局参数把别的流量也带偏了。
还有个容易忽略的点:全局参数只对遵循系统属性的库生效。比如老项目里常见的Apache HttpClient 4.x,默认不读这些属性,得手动调用 useSystemProperties() 才行。所以别以为配了启动参数就万事大吉,跑之前先验证一下请求到底从哪个出口出去的。
JDK原生写法:不引依赖也能跑
先看最原始的HttpURLConnection。新项目用得少了,但排查问题时特别好使,几行代码就能验证代理通不通:
// 无认证代理
Proxy proxy = new Proxy(Proxy.Type.HTTP,
new InetSocketAddress("代理服务器地址", 8080));
HttpURLConnection conn =
(HttpURLConnection) new URL("https://example.com/api").openConnection(proxy);
conn.setConnectTimeout(5000);
conn.setReadTimeout(10000);
System.out.println("响应码:" + conn.getResponseCode());
如果代理需要账号密码,得再挂一个全局Authenticator:
Authenticator.setDefault(new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication("用户名", "密码".toCharArray());
}
});
JDK 11之后自带的java.net.http.HttpClient写法现代得多,通过ProxySelector指定代理:
HttpClient client = HttpClient.newBuilder()
.proxy(ProxySelector.of(new InetSocketAddress("代理服务器地址", 8080)))
.connectTimeout(Duration.ofSeconds(5))
.build();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://example.com/api"))
.GET()
.build();
HttpResponse<String> response =
client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
主流框架接入:一个一个过
实际项目里大家用的HTTP库五花八门,下面把最常见的几个都过一遍,代码照抄就能用。
Apache HttpClient 5(企业项目里的常客):
HttpHost proxy = new HttpHost("代理服务器地址", 8080);
BasicCredentialsProvider credsProvider = new BasicCredentialsProvider();
credsProvider.setCredentials(
new AuthScope(proxy),
new UsernamePasswordCredentials("用户名", "密码".toCharArray()));
try (CloseableHttpClient client = HttpClients.custom()
.setProxy(proxy)
.setDefaultCredentialsProvider(credsProvider)
.build()) {
HttpGet get = new HttpGet("https://example.com/api");
try (CloseableHttpResponse resp = client.execute(get)) {
System.out.println(EntityUtils.toString(resp.getEntity()));
}
}
OkHttp(写法最简洁,认证要用专门的proxyAuthenticator):
Proxy proxy = new Proxy(Proxy.Type.HTTP,
new InetSocketAddress("代理服务器地址", 8080));
OkHttpClient client = new OkHttpClient.Builder()
.proxy(proxy)
.proxyAuthenticator((route, response) -> {
String credential = Credentials.basic("用户名", "密码");
return response.request().newBuilder()
.header("Proxy-Authorization", credential)
.build();
})
.connectTimeout(5, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS)
.build();
Request request = new Request.Builder().url("https://example.com/api").build();
try (Response response = client.newCall(request).execute()) {
System.out.println(response.body().string());
}
Spring RestTemplate(存量老项目改造最常见):
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setProxy(new Proxy(Proxy.Type.HTTP,
new InetSocketAddress("代理服务器地址", 8080)));
factory.setConnectTimeout(5000);
factory.setReadTimeout(10000);
RestTemplate restTemplate = new RestTemplate(factory);
Spring WebClient(响应式栈,底层走Reactor Netty):
HttpClient httpClient = HttpClient.create()
.proxy(spec -> spec.type(ProxyProvider.Proxy.HTTP)
.host("代理服务器地址")
.port(8080));
WebClient webClient = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
为了方便对照,整理成一张表:
| 框架/库 | 接入方式 | 适合场景 |
|---|---|---|
| HttpURLConnection | openConnection(proxy) | 小工具、快速验证 |
| JDK HttpClient | ProxySelector | JDK 11+轻量项目 |
| Apache HttpClient 5 | setProxy + 凭据提供器 | 企业级、需要连接池 |
| OkHttp | builder.proxy() | 代码简洁优先 |
| RestTemplate | RequestFactory设代理 | 存量Spring项目 |
| WebClient | Reactor Netty proxy | 响应式新项目 |
别硬编码:写个能轮换的简易代理池
真实采集任务里,一个IP用到底肯定撑不住,请求量一大就会被目标站限流。思路很简单:从服务商的API把可用IP列表拉下来存进内存,每次请求按顺序取一个,失效了就剔掉。整个流程如下图:
public class SimpleProxyPool {
private final List<Proxy> proxies = new CopyOnWriteArrayList<>();
private final AtomicInteger cursor = new AtomicInteger(0);
/** 从服务商API拉到IP列表后调用,每行格式:ip:port */
public void refresh(List<String> ipLines) {
List<Proxy> fresh = ipLines.stream()
.map(line -> line.split(":"))
.map(a -> new Proxy(Proxy.Type.HTTP,
new InetSocketAddress(a[0], Integer.parseInt(a[1]))))
.collect(Collectors.toList());
proxies.clear();
proxies.addAll(fresh);
cursor.set(0);
}
/** 轮询取下一个代理 */
public Proxy next() {
int size = proxies.size();
if (size == 0) throw new IllegalStateException("代理池是空的,先refresh");
return proxies.get(Math.floorMod(cursor.getAndIncrement(), size));
}
/** 请求失败时调用,把不稳定的IP请出池子 */
public void evict(Proxy bad) {
proxies.remove(bad);
}
}
再配一个定时任务自动补充弹药:
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
scheduler.scheduleAtFixedRate(() -> {
List<String> lines = fetchIpListFromApi(); // 调服务商提取接口
pool.refresh(lines);
}, 0, 1, TimeUnit.MINUTES);
这里有个细节值得注意:短效动态IP的存活时间通常只有几分钟,一次提取太多用不完也是浪费。按自己的并发量估算,够撑一个提取周期就行,多了反而白花钱。
踩坑记录:这四个问题我替你踩过了
坑一:认证方式搞混。服务商一般给两种认证——把你的服务器出口IP加白名单,或者用账号密码。白名单模式下代码里什么都不用配,但你的出口IP一变就全部报407;账密模式则要注意OkHttp的proxyAuthenticator和Apache的CredentialsProvider是两套完全不同的写法,别拿设置请求头的思路去硬套。
坑二:HTTPS请求莫名失败。HTTPS走代理时,客户端会先发CONNECT建立隧道,有些自己写的代理解析逻辑只处理了普通HTTP,遇到HTTPS就卡死。另外JDK全局参数里 https.proxyHost 和 http.proxyHost 是两个独立的键,只配一个就会出现"HTTP能通、HTTPS不通"的怪现象。
坑三:超时没分层设置。连接超时和读取超时要分开配。代理链路比直连多一跳,连接给5秒比较稳,读取按业务给10到30秒。不设超时的后果就是某台代理一抽风,整个线程池被活活拖死。
坑四:客户端对象反复创建。OkHttp和HttpClient内部都有连接池,每次请求new一个新客户端,等于亲手把池子废掉,还容易造成句柄泄漏。正确姿势是全局单例,需要不同代理时在请求级别覆盖。
服务商怎么挑:我的选型清单
框架配置只是后半截,IP质量才是决定任务成败的前半截。我挑服务商主要看三样:IP是不是正规授权来的(来路不明的资源用着提心吊胆)、可用率能不能稳在99%以上、提取接口对Java开发者友不友好。
拿我目前在用的神龙HTTP来说,3000万+正规授权代理IP、可用率99.9%,基础质量先有保障。最让我省心的是它的提取API兼容主流编程语言,返回的就是标准的ip:port列表,往上面那个SimpleProxyPool里一塞就能跑,官方文档连Java示例代码都备好了,照着写半天就能上线。另外个人中心有可视化的用量统计和实时监控,哪天成功率突然掉下来,一眼就能看出是哪个环节出了问题,不用对着日志一行行翻。
协议上HTTP、HTTPS、SOCKS5都支持,短效动态、长效静态、固定IP各有各的用处:高频取用新IP的采集任务配短效动态,需要长时间保持同一出口的业务配长效静态,按需选就行。技术支持是7×24小时的,半夜任务出状况也能找到人,这点对跑定时任务的后端来说很实在。
常见问题
Q: 配了代理之后请求反而变慢了?
A: 先分清慢在哪一环。本地用工具直连代理地址测一下握手耗时,如果代理本身响应就慢,换一个节点再试;如果代理快、业务慢,多半是读取超时设太短,或者目标站对这个代理出口有风控,把单IP的请求频率降下来看看。
Q: SOCKS5和HTTP代理在Java里配置差别大吗?
A: 代码结构基本一样,把 Proxy.Type.HTTP 换成 Proxy.Type.SOCKS 就行,JDK原生支持。区别在于SOCKS5工作在会话层,不解析应用层协议,某些场景下兼容性更好;但要注意OkHttp对SOCKS5不支持账号密码认证,有认证需求就选HTTP代理或换支持的库。
Q: 代理需要账号密码,但我不想把密码写死在代码里?
A: 标准做法是放进环境变量或配置中心,启动时注入。Spring项目可以用@ConfigurationProperties绑定一个ProxyProperties类,把host、port、username、password全部外置,不同环境配不同的值,代码里只依赖这个配置类,改密码不用重新发版。
Q: 怎么验证请求真的走了代理?
A: 最直接的办法是请求一个能回显出口IP的接口,对比返回的IP是不是代理的地址;也可以在本地抓包,看有没有向代理地址发起CONNECT或GET请求。上线前务必做这一步,我见过不少"以为配了、其实没生效"的案例,都是验证这步偷懒省掉的。






