最近在编写测试自动化代码时,由于项目测试需求,需要在代码中嵌入动态hosts文件修改并使之实时生效。对于hosts文件修改,就是简单的文件读和写,不做细说明(请直接看底部代码);本文重点说下如何让动态的hosts文件修改直接生效。
《一》 Java小白幼稚想法被现实扇了一巴掌
说到hosts文件修改后实时生效,在Windows下,手工方式,往往常用以下三种方式:
1. 重启浏览器
2. 在CMD中执行“ipconfig /flushdns”命令(可以不用重启浏览器)
3. 修改注册表,自动生效,只需页面刷新即可(如果是手动方式,一次修改即可一劳永逸)
在注册表中,HKeyCurrentUser\SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings下增加:
DnsCacheEnabled 0x0 (REG_DWORD)
DnsCacheTimeout 0x0 (REG_DWORD)
ServerInfoTimeOut 0x0 (REG_DWORD)
鉴于上面的方式,在实现完毕hosts文件修改后,如何使用Java来实现实时生效,泛起的第一个念头就是:用java直接调用Windows bat文件,可以即上面的提到的第二种方式不就OK了?立马查资料,发现用来调用DOS命令也挺简单的,代码如下:
try {
Runtime.getRuntime().exec("ipconfig /flushdns");
} catch (IOException e) {
System.out.println("ERROR: call runtime to flushdns exception: " + e.getMessage());
}
满怀开心的,运行下面测试代码,来进行测试验证。
public static void main(String[] args) throws IOException, InterruptedException, Exception {
String testURL = “www.taobao.com”;
String testHostIP = “10.232.35.181”;
System.out.println(InetAddress.getByName(testURL));
// 更新hosts文件,将淘宝首页域名指向指定的IP
updateHost(testHostIP, testURL);
// 调用DOS命令,进行host生效
Runtime.getRuntime().exec("ipconfig /flushdns");
System.out.println(InetAddress.getByName(testURL);
}
但尝试了几次,结果却并不如之前预期的那样?这是要闹那样,明明浏览器时这样work得好好的。到JAVA里咋就介么不给力呢~
《二》 万事问Google,柳暗花明又一村
懊恼没用啊,赶紧继续GOOGLE去,一查,原来Java有个东东叫DNS Caching in Java Virtual Machines. 它不像其他大部分的Stand-alone的桌面应用和网络应用一样,直接将系统的DNS Flush一下或重启就可以生效。Jdk为了提升系统性能,通过InetAddress将网络访问后的dns解析结果cache起来,并提供了以下方法来查询hostname和IP的匹配关系。
getAddress Returns the raw IP Address for this object.
getAllByName(String host) Given the name of host, an array of IP address is returned.
getByAddress(byte[] addr) Returns an InetAddress object given the raw IP address
getByAddress(String host, byte[] addr) Create an InetAddress based on the provided host name and IP address
getByName(String host) Determines the IP address of a host, given the host's name.
getCanonicalHostName() Gets the fully qualified domain name for this IP address.
getHostAddress() Returns the IP address string in textual presentation
getHostName() Gets the host name for this IP address
getLocalHost() Returns the local host.
(忍不住吐槽:妈的,说这么多,上面的这个东东同这次的主题“实时生效hosts文件修改“有毛关系)
除了InetAddress可以查询缓存的信息, Java中用了四个属性来管理JVM DNS Cache TTL(Time To Live),即DNS Cache的缓存失效时间。(这才有点像是要说到重点了~)
networkaddress.cache.ttl
- 缓存正确解析后的IP地址
- 指定的整数表明会缓存正确解析的DNS多长时间
- 默认值为-1, 代表在JVM启动期间会一直缓存
- 如果设置为0,则表示不缓存正确解析的结果
- 如果不设置,默认缓存30秒
networkaddress.cache.negative.ttl
- 缓存解析失败结果,可以减少DNS服务器压力
- 指定的整数表明会缓存解析失败结果多长时间
- 默认值为10,表示JVM会cache失败解析结果10秒
- 如果该值设置为0, 则表示不缓存失败结果
sun.net.inetaddr.ttl
- 私有变量,对应networkaddress.cache.ttl
- 这个参数,只能在命令行中被设置值。
sun.net.inetaddr.negative.ttl
- 私有变量,对应networkaddress.cache.negative.ttl,
- 同样,只能在命令行中被设置值。
《三》 不容易啊,终于切入正题
使用DNS Caching in Java Virtual Machines文中提到的方式,我们可以通过以下三种方式来进行对host修改后的实时生效:
-
编辑$JAVA_HOME/jre/lib/secerity/java.security文件中将网络地址缓存属性(networkaddress.cache.ttl和networkaddress.cache.negative.ttl)的值修改为你想要的值;优点是一劳永逸性的修改,非编程式的解决方案; 但java.security是公用资源文件,这个方式会影响这台机器上所有的JVM
-
在代码中可直接将动态配置,方式如下:
java.security.Security.setProperty(“propertyname”, “value”)
好处时,只影响当前的JVM,不影响他人,但缺点是,它是编程式的,
但正是利用了这一点,让我们的host文件修改可以实时生效
举个例子:
Security.setProperty("networkaddress.cache.ttl", "0");
Security.setProperty("networkaddress.cache. negative .ttl", "0");
-
在JVM启动时,在命令行中加入-Dsun.net.inetaddr.ttl=value and -Dsun.net.inetaddr.negative.ttl=value这两个指令,也可以起到配置缓存DNS失效时间作用。但注意这个方式,只在当networkaddress.cache.*属性没有配置时才能起作用。
-
除了上面提到的问题,我还找到一个资料,是通过Java反射机制,将InetAddresss类中的static变量addressCache强行修改方式,来达到实时生效hosts文件修改的目的。这种方式同上面提到的修改Java DNS Cache TTL不同,是不会去修改Cache配置,而是将运行中的缓存的IP/hostname对应关系数据强制修改。经过测试验证,确实可行。
public static void modifyDnsCachePolicy(String hostname) throws Exception {
// 开始修改缓存数据
Class inetAddressClass = InetAddress.class;
final Field cacheField = inetAddressClass.getDeclaredField("addressCache");
cacheField.setAccessible(true);
final Object obj = cacheField.get(inetAddressClass);
Class cacheClazz = obj.getClass();
final Field cacheMapField = cacheClazz.getDeclaredField("cache");
cacheMapField.setAccessible(true);
final Map cacheMap = (Map) cacheMapField.get(obj);
cacheMap.remove(hostname);
// 修改缓存数据结束
}
《四》 看完第三部分,你现在可以鄙视我,这个东东居然也会跌坑里。嘿嘿,不过问题总算解决了。还有些收获
- 下面是这次写的实现hosts文件修改的实现代码,支持功能有:
- 不破坏原有hosts文件,支持新host绑定或修改
- 支持host解绑
/**
* 获取host文件路径
* @return
*/
public static String getHostFile() {
String fileName = null;
// 判断系统
if ("linux".equalsIgnoreCase(System.getProperty("os.name"))) {
fileName = "/etc/hosts";
} else {
fileName = System.getenv("windir") + "\\system32\\drivers\\etc\\hosts";
}
return fileName;
}
/**
* 根据输入IP和Domain,删除host文件中的某个host配置
* @param ip
* @param domain
* @return
*/
public synchronized static boolean deleteHost(String ip, String domain) {
if (ip == null || ip.trim().isEmpty() || domain == null || domain.trim().isEmpty()) {
throw new IllegalArgumentException("ERROR: ip & domain must be specified");
}
String splitter = " ";
/**
* Step1: 获取host文件
*/
String fileName = getHostFile();
List<?> hostFileDataLines = null;
try {
hostFileDataLines = FileUtils.readLines(new File(fileName));
} catch (IOException e) {
System.out.println("Reading host file occurs error: " + e.getMessage());
return false;
}
/**
* Step2: 解析host文件,如果指定域名不存在,则Ignore,如果已经存在,则直接删除该行配置
*/
List<String> newLinesList = new ArrayList<String>();
// 标识本次文件是否有更新,比如如果指定的IP和域名已经在host文件中存在,则不用再写文件
boolean updateFlag = false;
for (Object line : hostFileDataLines) {
String strLine = line.toString();
// 将host文件中的空行或无效行,直接去掉
if (StringUtils.isEmpty(strLine) || strLine.trim().equals("#")) {
continue;
}
// 如果没有被注释掉,则
if (!strLine.trim().startsWith("#")) {
strLine = strLine.replaceAll("", splitter);
int index = strLine.toLowerCase().indexOf(domain.toLowerCase());
// 如果行字符可以匹配上指定域名,则针对该行做操作
if (index != -1) {
// 匹配到相同的域名,直接将整行数据干掉
updateFlag = true;
continue;
}
}
// 如果没有匹配到,直接将当前行加入代码中
newLinesList.add(strLine);
}
/**
* Step3: 将更新写入host文件中去
*/
if (updateFlag) {
try {
FileUtils.writeLines(new File(fileName), newLinesList);
} catch (IOException e) {
System.out.println("Updating host file occurs error: " + e.getMessage());
return false;
}
}
return true;
}
/**
* 根据输入IP和Domain,更新host文件中的某个host配置
* @param ip
* @param domain
* @return
*/
public synchronized static boolean updateHost(String ip, String domain) {
// Security.setProperty("networkaddress.cache.ttl", "0");
// Security.setProperty("networkaddress.cache.negative.ttl", "0");
if (ip == null || ip.trim().isEmpty() || domain == null || domain.trim().isEmpty()) {
throw new IllegalArgumentException("ERROR: ip & domain must be specified");
}
String splitter = " ";
/**
* Step1: 获取host文件
*/
String fileName = getHostFile();
List<?> hostFileDataLines = null;
try {
hostFileDataLines = FileUtils.readLines(new File(fileName));
} catch (IOException e) {
System.out.println("Reading host file occurs error: " + e.getMessage());
return false;
}
/**
* Step2: 解析host文件,如果指定域名不存在,则追加,如果已经存在,则修改IP进行保存
*/
List<String> newLinesList = new ArrayList<String>();
// 指定domain是否存在,如果存在,则不追加
boolean findFlag = false;
// 标识本次文件是否有更新,比如如果指定的IP和域名已经在host文件中存在,则不用再写文件
boolean updateFlag = false;
for (Object line : hostFileDataLines) {
String strLine = line.toString();
// 将host文件中的空行或无效行,直接去掉
if (StringUtils.isEmpty(strLine) || strLine.trim().equals("#")) {
continue;
}
if (!strLine.startsWith("#")) {
strLine = strLine.replaceAll("", splitter);
int index = strLine.toLowerCase().indexOf(domain.toLowerCase());
// 如果行字符可以匹配上指定域名,则针对该行做操作
if (index != -1) {
// 如果之前已经找到过一条,则说明当前line的域名已重复,
// 故删除当前line, 不将该条数据放到newLinesList中去
if (findFlag) {
updateFlag = true;
continue;
}
// 不然,则继续寻找
String[] array = strLine.trim().split(splitter);
Boolean isMatch = false;
for (int i = 1; i < array.length; i++) {
if (domain.equalsIgnoreCase(array[i]) == false) {
continue;
} else {
findFlag = true;
isMatch = true;
// IP相同,则不更新该条数据,直接将数据放到newLinesList中去
if (array[0].equals(ip) == false) {
// IP不同,将匹配上的domain的ip 更新成设定好的IP地址
StringBuilder sb = new StringBuilder();
sb.append(ip);
for (int j = 1; i < array.length; i++) {
sb.append(splitter).append(array[j]);
}
strLine = sb.toString();
updateFlag = true;
}
}
}
}
}
// 如果有更新,则会直接更新到strLine中去
// 故这里直接将strLine赋值给newLinesList
newLinesList.add(strLine);
}
/**
* Step3: 如果没有任何Host域名匹配上,则追加
*/
if (!findFlag) {
newLinesList.add(new StringBuilder(ip).append(splitter).append(domain).toString());
}
/**
* Step4: 不管三七二十一,写设定文件
*/
if (updateFlag || !findFlag) {
try {
FileUtils.writeLines(new File(fileName), newLinesList);
} catch (IOException e) {
System.out.println("Updating host file occurs error: " + e.getMessage());
return false;
}
}
return true;
}