这个故事从一个小的需求开始。
在知乎编辑器中,链接插入以后可以选择转化为一个链接卡片,用户体验太棒了。这么好的点子,我们必须学(chao)习(xi)过来啊。
这个事情就这么被安排给了我们的前端同学。。。。
但是没过多久,前端同学提出了需求。受限于浏览器的同源策略,在我站的Web编辑器上 是无法请求链接地址页面的title和icon的,需要后端支持出个接口,就很简单的把页面上的 <title> </title>
内容抓取一下返回就好了。
咔咔耗时30min,接口写完了 测试一下。very OK 上测服 前后端接口联调一下。
随着测试 问题开始暴漏了出来 外部站点的title获取一切正常。但是所有我司域名下的页面获取都失败了。
那这肯定不行啊,用户引用本站的资源时候反而不能渲染出链接卡片这是万万不能接受的了。
分析错误日志 发现了问题。在内部机器上 所有的*.csdn.net
子域名都被解析到了 类似 192.168.*.*
等内网IP上。不得不说,内网接口和外部接口使用同一个域名但是在不同网络环境下解析到不同服务真的是一个有点无(nao)奈(can)的设计。
那么该怎么解决呢。
先考虑下是怎么做到的相同域名访问到不同的IP的。在本地环境下。我们一般都是修改/etc/hosts文件(俗称绑个host)来做到的。在服务器的内网环境中。其实也是差不多原理 只是由在每台机器上写hosts文件变成了机器上制定DNS服务器(/etc/resolv.conf
)为内网DNS服务器。统一提供内网解析。
现在要做的。就是在对部分请求时。绕过系统提供的机器级别DNS服务器地址配置。对该请求使用单独的DNS服务器
思路已经有了,剩下的就是怎么实现了。又到了谷歌一下你就知道的时候了。
果然不负厚望的已经有大佬们给出了demo代码
我们只需要自己写一个DnsResolver 实现类重写其resolver方法即可控制每个域名被解析到的ip地址。
但是这个方案有一个问题。它需要硬编码穷举所有的希望自定义解析的域名对应的ip。而不是统一绕过本地DNS server配置使用制定的DNS服务器(如8.8.8.8)
在该需求背景下。如果穷举了目前需要正确解析到公网ip的域名地址,不可避免的会导致代码可维护性 -∞ 。像我这么认真负责的同学肯定不能把活儿干的这么糙啦。
那么接下来的思路就是再想办法搞点可以实现dns解析逻辑的代码。和上面找到的代码拼凑起来去实现这个需求。
dnsjava
就是它了。
然而 该库糟糕的文档导致进展一度很不顺利。
好在一番搜索之后终于找到了一个还算靠谱的demo。
https://springboot.io/t/topic/1611
最后 需要做的就是把dnsjava和httpclient给结合起来了。
贴下最终代码
Java httpClient中实现自定义DNS服务器地址配置
package net.csdn.ask.common.util.http;
import lombok.extern.slf4j.Slf4j;
import org.apache.hc.client5.http.DnsResolver;
import org.apache.hc.client5.http.SystemDefaultDnsResolver;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.cookie.StandardCookieSpec;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
import org.apache.hc.client5.http.socket.ConnectionSocketFactory;
import org.apache.hc.client5.http.socket.PlainConnectionSocketFactory;
import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;
import org.apache.hc.core5.http.URIScheme;
import org.apache.hc.core5.http.config.Registry;
import org.apache.hc.core5.http.config.RegistryBuilder;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.pool.PoolConcurrencyPolicy;
import org.apache.hc.core5.pool.PoolReusePolicy;
import org.apache.hc.core5.util.TimeValue;
import org.springframework.stereotype.Component;
import org.xbill.DNS.*;
import javax.annotation.PostConstruct;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Arrays;
import java.util.Objects;
@Component
@Slf4j
public class CustomDnsHttpUtil {
public static final String USER_AGENT = "Mozilla/5.0 Firefox/26.0";
private CloseableHttpClient client;
@PostConstruct
public void init() {
DnsResolver dnsResolver = new SystemDefaultDnsResolver() {
@Override
public InetAddress[] resolve(final String host) throws UnknownHostException {
return getDnsRealIp(host);
}
};
Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create()
.register(URIScheme.HTTP.id, PlainConnectionSocketFactory.getSocketFactory())
.register(URIScheme.HTTPS.id, SSLConnectionSocketFactory.getSocketFactory())
.build();
PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager(
socketFactoryRegistry,
PoolConcurrencyPolicy.STRICT,
PoolReusePolicy.LIFO,
TimeValue.NEG_ONE_MILLISECOND,
null,
dnsResolver,
null
);
client = HttpClients.custom()
.setConnectionManager(connManager)
.setUserAgent(USER_AGENT)
.build();
}
public InetAddress[] getDnsRealIp(String hostname) throws UnknownHostException {
try {
Resolver resolver = new SimpleResolver("223.5.5.5");
Lookup lookup = new Lookup(hostname, Type.A);
lookup.setResolver(resolver);
Cache cache = new Cache();
lookup.setCache(cache);
lookup.run();
if (lookup.getResult() == Lookup.SUCCESSFUL) {
Record[] records = lookup.getAnswers();
log.debug("getDnsRealIp | dnsServerName: {} | record: {}", hostname, records);
InetAddress[] inetAddressArr = Arrays.stream(records).map(record -> {
if (record instanceof ARecord) {
return ((ARecord) record).getAddress();
}
return null;
}).filter(Objects::nonNull).toArray(InetAddress[]::new);
log.debug("getDnsRealIp | dnsServerName: {} | inetAddressArr: {}", hostname, inetAddressArr);
return inetAddressArr;
}
} catch (Exception e){
log.error("getDnsRealIp error=====>", e);
}
throw new UnknownHostException("getDnsRealIp error | dnsServerName: " + hostname);
}
public String get(String url, Duration timeout) {
try {
HttpGet httpget = new HttpGet(url);
RequestConfig requestConfig = RequestConfig
.custom()
.setConnectTimeout(HttpClientUtil.timeout(timeout))
.setConnectionRequestTimeout(HttpClientUtil.timeout(Duration.ofMillis(100)))
.setResponseTimeout(HttpClientUtil.timeout(timeout))
.setCookieSpec(StandardCookieSpec.IGNORE)
.build();
httpget.setConfig(requestConfig);
CloseableHttpResponse response = client.execute(httpget);
return EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
} catch (UnknownHostException e) {
log.warn(e.getMessage());
} catch (Exception e) {
log.error("CustomDnsHttpUtil get error | url: {}", url, e);
}
return null;
}
}
PS: 心得感悟
在这个需求的实现过程中,并没有自己创造性的写几行代码。更多的是需要对http的请求原理过程,DNS的解析过程 有足够的了解,这样,才能在接到需求时有方案有想法。然后善用搜索引擎,让自己站在巨人的肩膀上快速准确的找到可以copy到自己项目中的代码,实现业务需求。最后,往往找到的资源都是零碎的,离你的目标会差了一步,要有意识的去把你的需求拆解为各个小的要求模块 分别找到这些模块的轮子。再去想办法把这些零碎的功能去拼凑起来,达到自己的需求。