Java如何校验IP/Domain格式

说到校验IP地址或者Domain的格式是否正确,很多人可能是想到用正则表达式来校验。
一种比较好的做法是使用现成的库来完成此功能,尤其是比较流行的类库,经过了大量的验证,比自己实现要安全可靠。
本文推荐使用Google的Guava库里的工具类来校验。

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>19.0</version>
</dependency>

校验IP地址格式的有效性

//com.google.common.net.InetAddresses#isInetAddress(String ipString)

InetAddresses.isInetAddress("1.1.1.1");//true
InetAddresses.isInetAddress("1.1.1");//false
InetAddresses.isInetAddress("127.0.0.1");//true
InetAddresses.isInetAddress("fc00::");//true
InetAddresses.isInetAddress("localhost");//false
InetAddresses.isInetAddress("256.256.1212.1");//false
InetAddresses.isInetAddress("256.256.257.1");//false
InetAddresses.isInetAddress("300.300.300.300");//false

//校验ip是ipv4地址还是ipv6地址
InetAddresses.forString("fc00::") instanceof Inet6Address;//true
InetAddresses.forString("127.0.0.1") instanceof Inet4Address;//true

校验Domain的合法性

//com.google.common.net.InternetDomainName#isValid(String name)

InternetDomainName.isValid("www.baidu.com"); //true
InternetDomainName.isValid("127.0.0.1"); //false
InternetDomainName.isValid("a.b.c.com"); //true
InternetDomainName.isValid("http://a.b.c.com"); //false
InternetDomainName.isValid("http://a.b.c.com/"); //false

Url的host部分合法性的校验

//com.google.common.net.HostSpecifier#isValid(String specifier)

//检查是否可以作为url的host部分,可能是domain或者ip
HostSpecifier.isValid("a.b.c.com");//true
HostSpecifier.isValid("a.b.c.com:90");//false
HostSpecifier.isValid("http://a.b.c.com");//false
HostSpecifier.isValid("127.0.0.1");//true
HostSpecifier.isValid("127.0.0.1:80");//false
HostSpecifier.isValid("111::");//true

javax.net.ssl.SSLHandshakeException: Received fatal alert: handshake_failure

最近在使用Java的HttpsURLConnection来访问https网站时,抛出了一个异常:

javax.net.ssl.SSLHandshakeException: Received fatal alert: handshake_failure

代码大致如下:

URL url = new URL("https://www.google.com/");
HttpsURLConnection connection = (HttpsURLConnection)url.openConnection();
connection.setHostnameVerifier((t, v) -> true);
BufferedInputStream inputStream = new BufferedInputStream(connection.getInputStream());
int respInt = inputStream.read();
while (respInt != -1) {
respInt = inputStream.read();
}
inputStream.close();

网上查了下,发现很多情况都可能导致抛出这个异常,一种可行的排查方法是,在执行Java程序时,加
-Djavax.net.debug=all 选项打开SSL连接时的debug开关,这样就会把建立连接的信息打印到控制台。

这样执行程序时可以在控制台打印类似的消息(部分摘取):

keyStore is :
keyStore type is : jks
keyStore provider is :
init keystore
init keymanager of type SunX509
trustStore is: /Library/Java/JavaVirtualMachines/jdk1.8.0_66.jdk/Contents/Home/jre/lib/security/cacerts
trustStore type is : jks
trustStore provider is :
init truststore
adding as trusted cert:
......
*** ClientHello, TLSv1.2
RandomCookie: GMT: 1527255937 bytes = { 72, 31, 116, 116, 76, 130, 134, 23, 59, 194, 6, 185, 160, 110, 14, 131, 74, 175, 192, 56, 83, 130, 176, 102, 53, 50, 126, 139 }
Session ID: {}
Cipher Suites: [TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, TLS_RSA_WITH_AES_128_CBC_SHA256, TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256, TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256, TLS_DHE_RSA_WITH_AES_128_CBC_SHA256, ...]
Compression Methods: { 0 }
Extension elliptic_curves, curve names: {secp256r1, sect163k1, sect163r2, secp192r1, secp224r1, sect233k1, sect233r1, sect283k1, sect283r1, secp384r1, sect409k1, ...}
Extension ec_point_formats, formats: [uncompressed]
Extension signature_algorithms, signature_algorithms: SHA512withECDSA, SHA512withRSA, SHA384withECDSA, SHA384withRSA, SHA256withECDSA, SHA256withRSA, SHA224withECDSA, SHA224withRSA, SHA1withECDSA, SHA1withRSA, SHA1withDSA, MD5withRSA
***
......
[Raw read]: length = 5
0000: 15 03 03 00 02 .....
[Raw read]: length = 2
0000: 02 28 .(
main, READ: TLSv1.2 Alert, length = 2
main, RECV TLSv1.2 ALERT: fatal, handshake_failure
main, called closeSocket()
main, handling exception: javax.net.ssl.SSLHandshakeException: Received fatal alert: handshake_failure

也就是在clientHello阶段,服务器端返回了握手失败。而这一步握手失败有很多原因:

  • 客户端和服务器端的cipher suites不兼容
  • 客户端和服务器端使用的ssl协议版本不一致。(sslv2, sslv3, tlsv1.0, tlsv1.1, tlsv1.2)
  • 服务器的根证书不在客户端的可信目录中

然而,在尝试了以上各种原因后,依然不行,正要崩溃时,在网上看到了关于JDK bug导致此错误的案例。

简单来说就是,JDK1.8的特定版本,如果自定义HttpsURLConnection的HostnameVerifier(类似上面的代码中的connection.setHostnameVerifier((t, v) -> true);)会导致握手的ClientHello阶段,client不发送SNI extension。(注意,可能你的代码中没有自定义,但是可能依赖的jar包中有自定义的操作)

正常情况下在ClientHello阶段,会发送:

Extension server_name, server_name: [type=host_name (0), value=www.google.com]

而在上面的出错的信息中,没有这个extension。

解决办法,升级到JDK1.8的8u152版本或者以上,或者降低到u66版本以下。

另外,如果不升级JDK,可以参见参考文档2来暂时规避掉此BUG.

另外,不是所有的网站都会出现这个问题,理论上来说与SNI相关的网站出问题的可能比较大。关于SNI(Server Name Indication),可以参考下:https://en.wikipedia.org/wiki/Server_Name_Indication

参考文档:

1)介绍了这类问题的参考思路:

https://stackoverflow.com/questions/6353849/received-fatal-alert-handshake-failure-through-sslhandshakeexception?utm_medium=organic&utm_source=google_rich_qa&utm_campaign=google_rich_qa

2)分析并解决了我这种情况的问题:

https://stackoverflow.com/questions/41692736/all-trusting-hostnameverifier-causes-ssl-errors-with-httpurlconnection

3)JDK bug详情:

https://bugs.java.com/view_bug.do?bug_id=JDK-8144566

 

转换dmesg中时间戳的方法

如果发现线上tomcat服务器进程突然没有了(可以通过ps aux | grep java来确认),一种可能是进程被内核给干掉了。

此时在命令行执行dmesg,可以看到类似信息:

[84368070.456270] Out of memory: Kill process 15166 (java) score 621 or sacrifice child
[84368070.456304] Killed process 15166, UID 59846, (java) total-vm:4077904kB, anon-rss:2515140kB, file-rss:612kB

这种情况,一般是服务器内存不足,然后内核把占内存最大的进程给杀掉了。

在上面这个信息中,最前面方括号里面的就是杀死进程的时间戳,这个数字84368070.456304的意思代表内核自从启动到现在经过的秒数,小数点后面精确到纳秒。这个数字很不直观,因此,需要转换成可读的时间表示。

方法1:(-T不是所有版本都支持,如果不支持,用方法2手动计算)

dmesg -T

方法2:

转换的思路很简单,[当前时间] – [内核启动后经过的秒数] + [dmesg的时间秒数]

date -d "1970-01-01 UTC `echo "$(date +%s)-$(cat /proc/uptime|cut -f 1 -d' ')+84368070.456304" | bc`seconds"

具体执行时,把84368070.45630换成你dmesg出来的时间戳即可。

也可以把以下代码放到文本文件里,作为校本方便以后使用。

date -d "1970-01-01 UTC `echo "$(date +%s)-$(cat /proc/uptime|cut -f 1 -d' ')+$1" | bc`seconds"

比如存为showDmesgTimestamp.sh,则可以直接执行

sh showDmesgTimestamp.sh 84368070.45630

即可打印可读形式的时间表示。

一次失败的移动号转电信号的经历

17年12月份,发现自己的移动账单话费比平时多了一倍,查了下发现自己的流量超了,顿时觉得移动太黑了,又查了下网上的互联网套餐,算了下确实很划算,愈发觉得移动黑心了,于是一怒之下,打客服电话把自己的移动号换成了8元套餐(每个月30分钟通话+100M流量),然后研究了各种互联网套餐,办了一个电信的大鱼号,月费19(100分钟通话+1G全国流量+阿里大文娱的app免流量),当然,作为一个iPhone用户,由于没有双卡双待,于是只能又买了一个功能机,这样就有了两个手机(功能机放移动号,iPhone放电信号)。

这个方案看似不错,一个用来上网,一个用来打电话,用这种方式过渡一下,最终把移动号淘汰。但是在实行起来时,发现太多的业务办理使用了移动号,甚至有些业务换个手机号非常麻烦,更甚至还在使用移动的亲情号,经常漏接电话,因为平时用得最多的拿在手里的是iPhone电信号,所以,在坚持了4个多月后,上周末去电信营业厅把电信号注销了,重新又回到了用移动号的日子。

一点经验:

1)如果你的手机支持双卡双待,用上述我的方案还是很划算的。曾经也想过换个双卡双待的手机,后来还是因为习惯了iPhone而作罢。

2)iPhone用户死心吧,除非哪天可以携号转网,否则安心的用原来的号吧。

结语:

折腾了这么久,也没省多少钱,想想自己也是可笑。

黄山归来

第一次去黄山是在2011年11月份,而自己一直记得是在2013年去的,去QQ空间的相册里看了下当时发的照片才确认了是11年,离现在竟然过去6年多了。

当时是和部门的同事一起去outting,具体的感受已经记不清了。大概的印象就是去泡了一个温泉,温泉的名字也不记得了,只记得是在半山腰的一个室外温泉。然后第二天去徒步爬黄山,没坐缆车,上下走了30多公里(依稀记得导游说的有这么远)。当时对黄山的印象大概就是,这是一个还会再来的地方,风景和视野都特别的赞。另外的印象,就是吃的特别差,导游安排的旅行餐,好难吃。以上就是11年去爬黄山的印象了。

上周五,又是跟部门出去团建,这次选择了黄山,不过这次的主要目的是去泡温泉,不是爬山。周五中午做大巴从hz出发,后来转景区车,大约在5点左右到了黄山的温泉酒店。这次再来黄山的感觉是经过了6年,黄山景区周围看起来还是很破旧的样子,并没有什么改善。由于经过了前几天的大雪,黄山的树上到处都是冰凌,非常漂亮。之后办理了酒店入住,就去吃饭了。晚饭后直奔温泉,开始泡温泉,泡完之后大概有这么个感觉,之前的连续的加班的疲惫,统统泡没了,浑身轻松,还是很值的。

次日早晨,起来酒店用过早饭后,和几个同事出去转转,因为中午就要回去了,所以今天没安排爬黄山,毕竟我们是来泡温泉的。不过,我们转着转着,还是往山上方向去了。到了慈光阁收费口,就下来了,一路过来,风景还是非常美的,尤其是难得的雪后的黄山,即使在下面看看,也是很赞的。中午饭后坐车回来了,很匆忙的行程。

这次的最大感觉,黄山依然很美,后面还会再来;黄山这边的人很淳朴;这次是自己点餐吃的还可以,比上次的导游订的旅行餐好多了;不过这边的经济发展还是慢了些,市容市貌比较脏乱,还有很大的进步空间。

无法在WordPress的iOS App上添加自建站点

下载了WordPress的iOS app,添加自己的站点。输入域名后,提示不是一个WordPress站点。

去网上搜到了这个:https://ios.forums.wordpress.org/topic/cant-add-my-website-into-wp-app

5楼Eric的回复,去http://xmlrpc.eritreo.it这个网址输入自己的站点域名验证下自己站点的xmlrpc是否正常。点击check后就可以看结果了。如果失败了的话,会有错误码。

我的站点当时是提示:-32700   parse error. not well formed

然后就搜索此信息,找到了这个文章:https://www.reddit.com/r/Wordpress/comments/5ly073/xmlrpc_throws_parse_error_not_well_formed_error/

按照提示:try adding the xml package: sudo apt-get install php-xml

安装php-xml就解决了。

https://ios.forums.wordpress.org/topic/xml-rpc-error-message-parse-error-not-well-formed 这里面也提示了相同的解决方式。

 

一个好用的IntelliJ IDEA的Maven插件

Maven工程,经常需要查找冲突的jar包,一个常用的方式是在命令行执行:

mvn dependency:tree 

然后在打印出来的依赖树中找到依赖的(包括间接依赖)的jar包的不同版本。
如果依赖较多的话,可以将其输出到一个文件中,然后便于查找,像这样:

mvn dependency:tree > dependency.txt

如果使用Intellij IDEA的话,可以安装Maven Helper插件。

具体可以在IDEA中依次点击:File-Settings-Plugins-browser repositories, 然后在搜索框输入 Maven Helper搜索,即可搜出来进行安装。安装后需要重启下。

安装完成后,就可以在IDEA中打开项目的pom.xml文件,在编辑器面板的左下角,可以发现多了一个TAB,Dependency Analyzer,点击切换到此面板,就可以在此查看冲突的jar包,也可以查找jar,通过点击查看依赖树,右击包名可以跳转到pom.xml文件中的对应位置,甚至可以直接exclude此二方包。更多功能可以自己安装后探索下, 或者参考附录的链接了解此插件的更多信息。

参考:
https://plugins.jetbrains.com/plugin/7179-maven-helper

win10运行Clojure脚本

像Python一样,用Clojure写一些日常要用的小脚本。

1)常规方法
前提:安装了JDK,并下载了clojure的jar包。
因为clojure本身是基于JVM的,而且它本身集成在一个jar包里,因此,执行clojure脚本可以直接通过java命令。
如下:

java -cp /path/to/clojure.jar clojure.main file1.clj arg1 arg2

在file1.clj中,可以通过*command-line-args*得到参数的list,比如(arg1, arg2)。

2)使用leiningen的插件lein-exec
前提:安装了JDK(v8.0)和Leiningen(v2.8.0)。
在~/.lein/profiles.clj(如果不存在,则创建此文件)中添加:

{:user {:plugins [[lein-exec "0.3.6"]]}}

然后在命令行执行lein version 或者其他的task,那么就会自动下载插件(默认下载的~/.m2/repository/lein-exec/目录下)。
此步如果卡着不动,可能是网络原因,可以从:https://mvnrepository.com/artifact/lein-exec/lein-exec/0.3.6 手动下载下来,放到本地对应目录下。

之后就可以通过

lein exec file1.clj arg1 arg2

来执行脚本文件了。在file1.clj中,可以通过*command-line-args*得到参数的list,但是与上面的常规方法不一样的是:它是包含脚本文件的名字的,比如(file1.clj, arg1, arg2)。

参考:
https://github.com/kumarshantanu/lein-exec
https://clojure.github.io/clojure/clojure.main-api.html

Guava的设计哲学

本文是自己学习Guava类库的一些学习心得,主要来自Guava的官方文档,并夹杂了一些自己的理解。
Guava是Google内部使用的核心Java类库的开源版本,这些核心实用类都是Google内部员工平时编码中经常使用的类库,已经在Google的生产环境验证,因此,我们可以放心的使用他们。
Guava的定位是使得用Java语言工作更加愉快和更加高效。JDK自带的utilities类(比如Collections类)被广泛应用并极大的简化了Java代码,Guava的目标就是要继续这个“传说”。
Effective Java第47条,“了解并使用类库”,告诉我们,使用类库比自己写一个实用工具类更可取。(个人觉得,如果为了学习,这条可以忽略,相反,自己写一个类库并且跟现有的优秀的类库比较,反而更利于个人成长。)
【总之,不要重复造轮子。如果你需要一些看起来很通用的功能,那么可能已经存在相应的类库实现了你的功能。如果存在,那么就使用它;如果你不知道,那么可以去搜下。总之,使用类库大部分情况比自己实现要更好。这个无关你作为一个程序员的能力,因为通用的类库集聚了大部分程序员的努力,肯定比你自己实现这个功能投入的精力要多。】(摘自effective java)
所以,不得不说:
  • Guava在Google的生产环境经过了实战测试。
  • Guava经过了很多的单元测试。自动化的测试,并且覆盖率很高,尤其是collect子包。
  • Guava开发很活跃,用户参与也很积极。
  • 总之,很牛逼。
Guava在增加特性方面偏保守,在决定是否增加一个特性时,会考虑是否‘实用’并且‘普遍存在’。‘实用’表现在:可以节省你大量的代码;避免让你写出很难调试或者易于出错的代码;提高可读性;提高速度。‘普遍存在’就是在代码中竟然被使用,这一项可以通过Google内部的代码库里扫描统计出来。
相对于增加特性方面的保守,Guava在去除不需要的特性方面很果断,会把这些特性标为deprecate继而删除该特性。因此,使用@Beta注解的类或者方法要谨慎使用。
Guava的实现针对一些通用的设计原则,包括:
  • 针对特定的场景,这些api是最优的解决方案;
  • 类或者方法的语义是可以从他们的签名上很明显很直观的看出来。实现可以很smart,但是名字要很直观。
  • 鼓励Guava的用户有好的代码习惯,而Guava它自己的代码本身也是好代码习惯的典范。(比如快速失败,拒绝null)
  • 不要试图单独解决每一种场景,相反提供通用的工具以解决我们可能没遇到过的场景。
  • 强调可维护性,并留出未来重构的空间。(结果:大部分暴露出去的类应该是final的,就像effective java 17条说的;把AbstractXXX类暴露出去也应该很谨慎。)

Maven报AetherClassNotFound异常

maven 3.3 执行mvn dependency:tree时,会报出如下异常:

Error injecting: org.apache.maven.shared.dependency.graph.internal.Maven3DependencyGraphBuilder java.lang.NoClassDefFoundError: org/sonatype/aether/graph/Dependency

原因是因为,在Maven 3.1-alpha-1 从Sonatype Aether迁移到了Eclipse Aether,而这个变更对于某些插件(比如maven-dependency-plugin, android-maven-plugin, maven-shade-plugin, maven-site-plugin)是不兼容的。所以会报出找不到相关的类。

解决方法是:使用Maven 3.1.*之前的版本,比如3.0.5。或者根据如下链接,升级对应的插件的版本:https://cwiki.apache.org/confluence/display/MAVEN/AetherClassNotFound