做Java数据采集的朋友应该都遇到过这么个情况:你写好了一个HttpURLConnection或者HttpClient的请求,本地跑得好好的,结果一上量,目标站点直接给你403或者弹验证码。说白了,你的出口IP被标记了。这时候你需要的不是换台机器,而是给HTTP请求挂上一个动态IP代理,让每次请求走不同的出口。
这篇文章我就从最底层的HttpClient讲起,一路讲到Spring Boot项目里怎么把代理IP配置得干干净净。中间会涉及到动态IP的轮换逻辑、连接池管理这些实际开发中绕不开的东西。如果你正在用Java做公开数据的采集工作,这篇应该能帮你省掉不少调试时间。
先搞清楚你手里的代理IP是什么形态
在写代码之前,得先明白动态IP代理到底是怎么工作的。你可以把它理解成一个"中转站":你的Java程序不直接连目标服务器,而是先把请求发给代理服务器,代理服务器用自己的IP去访问目标,再把结果原路返回给你。
动态IP的核心特征就一个字——换。每次你向代理服务商的API要一个IP,拿到的出口地址都不一样,而且这个IP的存活时间很短,通常几分钟到半小时。这意味着你的请求在目标站点看来,永远来自不同的"人",不会触发频率限制。
我目前项目里用的是神龙HTTP的短效动态IP池,他们的IP资源来自国内三大运营商的正规授权,池子里有3000万+的IP,每天更新去重。我一般用5分钟有效期的那种,够用且稳定。他们的API接口对Java开发者很友好,文档里直接给了各语言的调用示例,不用自己猜参数格式。如果你需要指定某个省份或城市的出口IP,他们支持300+城市级定位,这个在做区域化数据采集的时候特别实用。
拿到代理IP之后,格式通常长这样:
代理地址: 120.234.xx.xx
代理端口: 8080
有效期: 300秒(5分钟)
协议: HTTP
后面所有代码示例都基于这个格式来写。
最基础的方式:HttpClient手动设置代理
如果你还没上Spring Boot,或者就是在写个独立的工具类,Apache HttpClient是最直接的方案。核心就两步:创建一个HttpHost对象指向你的代理,然后把它塞进HttpClientBuilder里。
import org.apache.http.HttpHost;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
public class ProxyHttpClientDemo {
public static String fetchWithProxy(String targetUrl, String proxyHost, int proxyPort) throws Exception {
// 这一步是关键:告诉HttpClient走哪个代理出去
HttpHost proxy = new HttpHost(proxyHost, proxyPort, "http");
CloseableHttpClient client = HttpClients.custom()
.setProxy(proxy)
.build();
HttpGet request = new HttpGet(targetUrl);
// 加个User-Agent,别用默认的Apache-HttpClient/4.x,太容易被识别
request.setHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
try (CloseableHttpResponse response = client.execute(request)) {
return EntityUtils.toString(response.getEntity(), "UTF-8");
} finally {
client.close();
}
}
public static void main(String[] args) throws Exception {
// 假设你从神龙HTTP的API拿到了这个代理
String proxyIp = "120.234.xx.xx";
int proxyPort = 8080;
String html = fetchWithProxy("https://example.com/data", proxyIp, proxyPort);
System.out.println(html.substring(0, 200));
}
}
这里有个容易忽略的点:每次请求完记得close掉client。如果你在一个循环里反复创建HttpClient而不关闭,连接池会泄漏,跑着跑着就OOM了。如果请求量不大,上面的写法没问题;如果量大,建议把client提出来复用,只换proxy参数。
OkHttp的写法:更轻量一点
有些项目里用OkHttp比较多,它的代理配置稍微简洁一些。OkHttp的Proxy类直接继承自JDK的java.net.Proxy,所以写法很直观:
import okhttp3.;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.util.concurrent.TimeUnit;
public class OkHttpProxyDemo {
public static OkHttpClient buildClient(String proxyHost, int proxyPort) {
Proxy proxy = new Proxy(Proxy.Type.HTTP,
new InetSocketAddress(proxyHost, proxyPort));
return new OkHttpClient.Builder()
.proxy(proxy)
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(15, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.build();
}
public static String doRequest(String url, String proxyHost, int proxyPort) throws Exception {
OkHttpClient client = buildClient(proxyHost, proxyPort);
Request request = new Request.Builder()
.url(url)
.header("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)")
.build();
try (Response response = client.newCall(request).execute()) {
return response.body().string();
}
}
}
OkHttp的好处是它的连接池管理做得比较自动化,你不用太操心底层的socket回收。但注意一点:OkHttp的OkHttpClient实例是线程安全的,可以全局复用一个,不要每次请求都new一个,那样TLS握手开销会很大。
Spring Boot项目里怎么优雅地配置代理
到了Spring Boot层面,你肯定不想把代理IP硬编码在业务代码里。我的做法是分层:代理IP的获取和轮换放在一个独立的Service里,业务层只关心"给我一个能用的代理"。
先说配置文件。在application.yml里把代理相关的参数抽出来:
application.yml
proxy:
神龙HTTP的API地址(从他们控制台获取)
api-url: "https://api.shenlongip.com/getip"
api-key: "你的API密钥"
默认代理端口
default-port: 8080
代理协议
protocol: "http"
超时配置(毫秒)
connect-timeout: 10000
read-timeout: 15000
然后写一个配置类,把这些参数注入到Spring容器里:
@Configuration
@ConfigurationProperties(prefix = "proxy")
@Data
public class ProxyConfig {
private String apiUrl;
private String apiKey;
private int defaultPort;
private String protocol;
private int connectTimeout;
private int readTimeout;
}
接下来是核心部分——代理IP的获取和轮换。我封装了一个ProxyProvider,它负责跟神龙HTTP的API打交道,每次调用返回一个新的代理地址:
@Service
public class ProxyProvider {
@Autowired
private ProxyConfig proxyConfig;
private final RestTemplate restTemplate = new RestTemplate();
/
从神龙HTTP获取一个新的动态代理IP
返回格式: "ip:port"
/
public String getNewProxy() {
String url = proxyConfig.getApiUrl()
+ "?key=" + proxyConfig.getApiKey()
+ "&protocol=" + proxyConfig.getProtocol()
+ "&count=1";
try {
ResponseEntity response = restTemplate.getForEntity(url, String.class);
// 返回体一般是 "ip:port" 格式,多行则每行一个
String body = response.getBody().trim();
// 取第一个
return body.split("\")[0].trim();
} catch (Exception e) {
throw new RuntimeException("获取代理IP失败", e);
}
}
/
获取一个配置好代理的HttpClient
/
public CloseableHttpClient buildProxiedClient() throws Exception {
String proxyStr = getNewProxy();
String[] parts = proxyStr.split(":");
HttpHost proxy = new HttpHost(parts[0], Integer.parseInt(parts[1]), "http");
return HttpClients.custom()
.setProxy(proxy)
.setConnectionTimeToLive(280, TimeUnit.SECONDS) // 比IP有效期短一点
.build();
}
}
业务层用起来就很清爽了:
@Service
public class DataFetchService {
@Autowired
private ProxyProvider proxyProvider;
public String fetchData(String targetUrl) throws Exception {
// 每次请求拿一个新的代理,用完就扔
CloseableHttpClient client = proxyProvider.buildProxiedClient();
try {
HttpGet get = new HttpGet(targetUrl);
get.setHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)");
try (CloseableHttpResponse resp = client.execute(get)) {
return EntityUtils.toString(resp.getEntity(), StandardCharsets.UTF_8);
}
} finally {
client.close(); // 用完必须关
}
}
}
这里我特意把setConnectionTimeToLive设成了280秒,比神龙HTTP短效IP的300秒有效期短20秒。为什么?因为如果你让连接活到IP过期,后半段请求会走一个已经失效的代理,目标站点那边看到的就是连接中断或者超时。提前20秒回收连接,让下一次请求重新拿新IP,能避免这个坑。
动态IP轮换:高并发场景下的处理
如果你的采集任务不是串行跑,而是用线程池并发请求,那代理IP的管理就不能这么简单了。我一般用ConcurrentLinkedQueue做一个简易的IP池:
@Service
public class ProxyPoolService {
@Autowired
private ProxyProvider proxyProvider;
// 预热的代理IP池
private final Queue ipQueue = new ConcurrentLinkedQueue<>();
@PostConstruct
public void init() {
// 启动时预热20个IP
for (int i = 0; i < 20; i++) {
ipQueue.offer(proxyProvider.getNewProxy());
}
}
/
从池中取一个IP,取完自动补充
/
public String pollProxy() {
String ip = ipQueue.poll();
if (ip == null) {
// 池空了,现取一个
ip = proxyProvider.getNewProxy();
}
// 异步补充(这里简化,实际可以用@Async)
ipQueue.offer(proxyProvider.getNewProxy());
return ip;
}
}
线程池里每个worker取IP的时候调用pollProxy()就行,不用每次都去调API,减少网络开销。神龙HTTP的API支持并发提取,但没必要每个请求都打一次,预热池的方式更稳。
如果你的并发量特别大,比如几百个线程同时在跑,我建议看看神龙HTTP的企业定制池方案。他们的大客户经理会一对一聊你的业务场景和用量,帮你定制专属的IP池和提取策略,技术团队7×24小时在线,遇到并发瓶颈或者IP可用率波动的问题可以直接找他们调。比自己硬扛要省心很多。
几个实际开发中容易踩的坑
坑一:代理IP和直连混用。 有些同学图省事,代码里写了一个HttpClient,有时候设代理有时候不设。结果就是同一个连接池里既有走代理的连接也有直连的,目标站点一看IP跳来跳去,直接封你。要么全走代理,要么全直连,别混。
坑二:超时时间设得太长。 动态IP的存活时间就几分钟,如果你read timeout设了60秒,IP可能在你还没读完响应的时候就已经过期了。建议connect timeout不超过10秒,read timeout不超过20秒,具体看你的目标站点响应速度。
坑三:忽略HTTPS场景。 如果你的目标站点是HTTPS的,代理协议也要用HTTPS或者SOCKS5。神龙HTTP支持HTTP/HTTPS/SOCKS5三种协议,你在API请求的时候指定protocol=https就行,Java代码里HttpHost的scheme改成"https"即可。别用HTTP代理去访问HTTPS站点,会报SSL握手错误。
坑四:没做失败重试。 动态IP虽然可用率很高(神龙HTTP标称99.9%),但总有极小概率某个IP刚好不可用。生产环境里一定要加重试逻辑,失败后换一个IP再试,别一次失败就抛异常:
public String fetchDataWithRetry(String targetUrl, int maxRetries) {
for (int i = 0; i < maxRetries; i++) {
try {
String proxy = proxyPoolService.pollProxy();
String[] parts = proxy.split(":");
HttpHost proxyHost = new HttpHost(parts[0], Integer.parseInt(parts[1]), "http");
CloseableHttpClient client = HttpClients.custom()
.setProxy(proxyHost)
.build();
try {
HttpGet get = new HttpGet(targetUrl);
try (CloseableHttpResponse resp = client.execute(get)) {
if (resp.getStatusLine().getStatusCode() == 200) {
return EntityUtils.toString(resp.getEntity(), StandardCharsets.UTF_8);
}
}
} finally {
client.close();
}
} catch (Exception e) {
log.warn("第{}次请求失败,换IP重试: {}", i + 1, e.getMessage());
}
}
throw new RuntimeException("重试" + maxRetries + "次后仍然失败");
}
不同场景下选哪种IP类型
不是所有场景都适合用短效动态IP。简单列一下我遇到的几种情况:
| 场景 | 推荐IP类型 | 原因 |
|---|---|---|
| 高频采集、需要频繁换出口 | 短效动态IP(3~30分钟) | IP轮换快,不容易被目标站点识别为同一来源 |
| 需要持续跟踪某个数据源、IP不能频繁变 | 长效静态IP(1~24小时) | IP存活时间长,同一小时内请求看起来来自同一地址,逻辑更连贯 |
| API对接、需要固定出口IP做白名单 | 固定IP | IP长期不变,对方可以加白名单,连通率很高 |
| 大规模并发、业务逻辑复杂 | 企业定制池 | 专属资源池,不跟其他用户抢IP,稳定性有保障 |
神龙HTTP这几类都有,而且计费方式灵活,包量包时都能选。我一般日常采集用短效动态IP,偶尔需要固定出口的场景用固定IP,按个数买几个就够用了,不用长期占着。
常见问题
Q1:我的Java程序设置了代理,但请求还是走的本机IP,怎么排查?
先检查三件事:第一,HttpHost的scheme对不对,HTTP代理写"http",HTTPS代理写"https",写错了会静默失败;第二,你的目标URL是不是用了http://开头,如果你用HTTP代理去请求一个HTTPS地址,Java底层会忽略代理设置直接连;第三,打印一下实际发出的请求日志,确认代理参数确实传进去了。另外确认你的代理IP还在有效期内,神龙HTTP的短效IP过期后就会失效,这时候请求会直接超时而不是走代理。
Q2:并发跑的时候,多个线程共用一个HttpClient会不会有问题?
Apache HttpClient的CloseableHttpClient本身是线程安全的,可以多个线程共用。但问题是——如果你共用一个client,那所有线程走的都是同一个代理IP,这就失去了动态IP的意义。正确做法是每个线程(或每次请求)创建独立的client实例,各自绑定不同的代理IP。用完close掉。如果担心创建开销,可以用线程本地变量(ThreadLocal)让每个线程持有自己的client,线程池固定大小的话开销可控。
Q3:神龙HTTP的API返回的IP偶尔不可用,怎么在代码里处理?
他们的IP可用率标称99.9%,但网络环境复杂,偶尔一个IP连不上很正常。代码层面做好两件事就够了:一是加超时控制,connect timeout设短一点(5~10秒),连不上就快速失败;二是加重试,失败后从池子里取下一个IP再试,一般重试2~3次就能拿到可用的。如果连续多次都失败,大概率是网络层面出了问题(比如你的服务器到代理节点之间链路抖动),这时候可以联系神龙HTTP的技术支持,他们7×24小时在线,能帮你查具体是哪个节点的问题。
Q4:Spring Boot项目里,代理配置能不能做到不同环境用不同的代理池?
可以。用Spring Profile就行。在application-dev.yml里配开发环境用的代理参数,application-prod.yml里配生产环境的。比如开发环境你用量小,用短效动态IP的包时套餐;生产环境并发大,切到企业定制池的API地址。代码里通过@Value或者@ConfigurationProperties注入,切换profile就自动换配置,不用改代码。神龙HTTP的API地址和key在不同套餐下是不一样的,你从他们控制台对应套餐的页面拿就行。


