引言:如果你也是开发者的话,你很可能已经知道PoLA法则(Principle of Lease Astonishment)。那么,看看这篇文章讲述的充满奇幻色彩的调试经历,来见识一下PoLA是如何与HttpURLConnection发生了关联。

如果你和我一样也是开发者的话,你很可能已经听说过“PoLA”原则,或者叫作“产生最少意外”原则。意思非常简单,就是不要让你的用户感到惊讶。或者更明确一些,就像本文这种情况,不要让另外一个开发者感到惊讶。不幸的是,我上个星期就遇到了大大超出我意外的事情,我们有个服务的客户调用端总是发出一些垃圾的请求。

你说垃圾请求吗?是的,就像这样,我们完全不清楚这些请求是从哪里来的。又是这样一个时刻,经理们毫无头绪,抱头乱窜,惊呼“我们肯定是被黑客攻击了”,或者 ”有人把防火墙给关掉了!!”

无论如何,先说点背景情况吧,我们的项目里有自动记录活动日志的功能,当某些情况下,比如一个进程启动的时候就会进行记录。这包括我们那出问题的网络服务客户端和服务端,因为它们两者都属于系统的一部分。在某些时候,我们注意到,服务端的响应还没有发出的时候,另外一个来自同样客户端的请求又发了过来。这个真是出乎意料的,因为客户端代码是单线程的,也没有其他的客户端掺和进来。审查代码、测试之后,结论是我们的客户端不可能在第一个请求还没结束的时候再同时发出另外一个。

经过一整天的调试和研究日志发现,事实上,在服务端处理还未结束的时候客户端其实已经断开连接了。所以,这些请求终究并不是同时发生的,但是为什么我们花了一整天的时间才发现呢?这跟我们玩了一整天的星球大战有啥区别?

好吧,其实也不是。我们发现了罪魁祸首,服务端的容器软件HTTP的读超时设置被调得太低了。服务端的日志显示的确生成了响应,但是客户端却在此之前已经断开了,因为服务器端发生了读超时。这些在服务器端当然没有日志记录,因为这种行为是更低一层协议决定的(HTTP栈),而不是服务端的应用代码。

是的,没错,我听明白了,但是客户端的日志该怎么解释?客户端是不是应该抛出一个“ReadTimeoutException”异常,或者类似的玩意,然后可以写到日志里?然而,没错,事实上,并没有。就像现在发现的一样,真正的意外来自HttpURLConnection类的内部(更确切地说,是默认的Oracle的官方实现sun.net.www.protocol.http.HttpURLConnection)。

你以前是否知道HttpURLConnection的默认实现有个在某些情形下自动重试的特性?好吧,我之前就不知道。当时的情况是,客户端的确触发了超时异常,但是却被HttpURLConnection给捕捉了,而它自己决定重新尝试一次。这就意味着,你调用了HttpURLConnection的read()方法,它阻塞了,你正在等待,看起来就好像是在等待第一次请求的响应一样。但是在HttpURLConnection内部,它作了不止一次尝试,因此创建了不止一个socket连接。这就解释了为什么第二次及以后的请求永远在日志里找不到,因为这些第二次之后的请求是HttpURLConnection内部发起的。

让我们上一些代码重现一下。

import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.util.concurrent.Executors;
import com.sun.net.httpserver.HttpServer;
/**
* Created by koen on 30/01/16.
*/
public class TestMe {
public static void main(String[] args) throws Exception {
startHttpd();
HttpURLConnection httpURLConnection = (HttpURLConnection) new URL(“http://localhost:8080/“).openConnection();
if (!(httpURLConnection instanceof sun.net.www.protocol.http.HttpURLConnection)) {
throw new IllegalStateException(“Well it should really be sun.net.www.protocol.http.HttpURLConnection. ”
+ “Check if no library registered it’s impl using URL.setURLStreamHandlerFactory()”);
}
httpURLConnection.setRequestMethod(“POST”);
httpURLConnection.connect();
System.out.println(“Reading from stream…”);
httpURLConnection.getInputStream().read();
System.out.println(“Done”);
}
public static void startHttpd() throws Exception {
InetSocketAddress addr = new InetSocketAddress(8080);
HttpServer server = HttpServer.create(addr, 0);
server.createContext(“/”, httpExchange -> {
System.out.println(“——> Httpd got request. Request method was:” + httpExchange.getRequestMethod() + ” Throwing timeout exception”);
if (true) {
throw new SocketTimeoutException();
}
});
server.setExecutor(Executors.newCachedThreadPool());
server.start();
System.out.println(“Open for business.”);
}
}
运行之,将会得到类似下面的输出。

Open for business.
Reading from stream…
——> Httpd got request. Request method was:POST Throwing timeout exception
——> Httpd got request. Request method was:POST Throwing timeout exception
Exception in thread “main” java.net.SocketException: Unexpected end of file from server
at sun.net.www.http.HttpClient.parseHTTPHeader(HttpClient.java:792)

注意,我们的监听服务被调用了两次,但是我们只发了一个请求。如果我们加上-Dsun.net.http.retryPost=false这个属性再运行一次的话,我们会得到下面的输出:

——> Httpd got request. Request method was:POST Throwing timeout exception
Exception in thread “main” java.net.SocketException: Unexpected end of file from server
at sun.net.www.http.HttpClient.parseHTTPHeader(HttpClient.java:792)

好,先把这事放一边,我想问的是,到底是谁搞出这么个设计来,既没文档描述又没有可配置选项?为啥我做了十五年的Java开发,却对此一无所知?更要命的是,为什么它要对一个构造异常的POST请求进行重试呢?这是对PoLA赤裸裸的违背!

现在你可能已经猜到了,这是一个BUG(链接:http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6382788)。当然了,说是BUG并不是指的它的重试机制,而是指它为什么对异常POST请求也会进行重试。按照HTTP RFC的规范,POST请求并非幂等,因此多次提交POST会带来服务器端数据的改变。但是别担心,Bill早就把这个BUG修改好了。Bill的解决方法是加了一个开关。Bill了解向后兼容原则。Bill认为最好的方法是添加一个默认开启的开关,这样可以保证这个BUG的向后兼容。Bill笑了。Bill已经能够看见全球无数的Java开发者掉进这个大坑时惊愕的面孔。但是,你们都别学Bill好吗?

经过好几天激动人心的调试,最后问题解决的方式却略显轻巧,仅仅指定了一个属性就搞定了。无论如何,这个设计真是着实让我很意外,因此我还专门写了这篇文章来讲述,并且,你也看到了这篇文章。

为了完整起见,再提醒一下,如果你让这段代码在容器环境里执行的话,结果可能会不同。你的容器或者你的代码所依赖的库有可能会替换掉Oracle默认的内部实现,请参考URL.setURLStreamHandlerFactory()。现在你可能会问,那个家伙当时为什么要使用HttpURLConnection呢?他难道是坐着演讲巡游车上班吗(原文Wooden Soapbox,由来参见https://en.wikipedia.org/wiki/Soapbox)?他难道是用剪子来割草吗?建议他传递信息的时候最好还是使用烽火吧!当然了,你这么想我也不能责怪你。我们出问题的代码有点特别,使用的是SAAJ中的SOAPConnectionFactory,而SOAPConnectionFactory内部又默认使用了HttpURLConnection,如果没有其他代码来注册其他的实现类的话,使用的当然就是默认的Oracle实现喽~

如果你使用其他更专业的web服务实现的时候(如Spring WS, CXF, JAX-WS实现等等),他们很可能使用了诸如Apache HTTP Client的组件。当然了,如果你自己的代码需要发起HTTP连接的话,你也可以使用它。没错,我还是推荐你使用Apache Commons HttpClient,虽然这货修改API的频率比普通时尚达人换鞋的频率都还要高。好了,我的牢骚完了。

译文链接:http://www.codeceo.com/article/java-httpurlconnection-pola.html
英文原文:HttpURLConnection vs. the Principle of Least Astonishment
翻译作者:码农网 – Sandbox Wang
[ 转载必须在正文中标注并保留原文链接、译文链接和译者等信息。]

Java 中 HttpURLConnection 与 PoLA 法则相关推荐

  1. Java中HttpURLConnection 与 PoLA 法则

    如果你和我一样也是开发者的话,你很可能已经听说过"PoLA"原则,或者叫作"产生最少意外"原则.意思非常简单,就是不要让你的用户感到惊讶. 或者更明确一些,就像 ...

  2. 安全证书导入到java中的cacerts证书库

    提示: javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path buildi ...

  3. 深入Java中的位操作

    来源:https://0x9.me/I3YJk 引 学完本章节你将学会位的基础概念与语法,并且还会一些骚操作!! 与.或.非.位移 原码.反码.补码 字节.位.超区间...... 开始本章节之前,我们 ...

  4. Java中继承、接口、多态的作用详解(纯理论)

    一.继承.接口与多态的相关问题: 1. 继承的作用?好处?坏处? 继承:通过继承实现代码复用.Java中所有的类都是通过直接或间接地继程java.lang.Object类得到的.继承而得到的类称为子类 ...

  5. Java 中如何模拟真正的同时并发请求?

    有时需要测试一下某个功能的并发性能,又不要想借助于其他工具,索性就自己的开发语言,来一个并发请求就最方便了. java中模拟并发请求,自然是很方便的,只要多开几个线程,发起请求就好了.但是,这种请求, ...

  6. java 反查域名_C段查询雏形之在Java中反查一个IP上的所有域名(旁站查询)

    这里使用了两个接口来反查IP,分别是"站长工具"和"爱站"的接口,两者各有千秋,结合起来查询就较为准确了. 注:我目前只写了个初始版本,还不太完善,但是可以基本 ...

  7. java http请求 乱码_怎么解决java中的http请求乱码

    怎么解决java中的http请求乱码 发布时间:2020-06-23 20:00:11 来源:亿速云 阅读:90 作者:元一 怎么解决java中的http请求乱码?针对这个问题,今天小编总结了这篇文章 ...

  8. 浅析Java中对象的创建与对象的数据类型转换

    这篇文章主要介绍了Java中对象的创建与对象的数据类型转换,是Java入门学习中的基础知识,需要的朋友可以参考下 Java:对象创建和初始化过程 1.Java中的数据类型     Java中有3个数据 ...

  9. java 中的 Scanner

    java.util.Scanner 是 Java5 的新特征,主要功能是简化文本扫描.这个类最实用的地方表现在获取控制台输入,其他的功能都很鸡肋,尽管 Java API 文档中列举了大量的 API 方 ...

最新文章

  1. java jackson包_java json工具包Jackson的使用
  2. 光遇自动弹琴脚本代码_光遇弹琴辅助软件下载-光遇自动弹琴脚本代码下载v1.0_86PS软件园...
  3. 每日英语:Delayed Development: 20-Somethings Blame The Brain
  4. 【C#-枚举】枚举的使用
  5. oracle之数据处理之其他数据库对象
  6. Android 微信登录
  7. mysql分布式数据库架构_MySQL分布式数据库架构:分库、分表、排序、分页、分组、实现教程...
  8. 【定有惊喜】android程序员如何做自己的API接口?php与android的良好交互(附环境搭建),让前端数据动起来~...
  9. 力扣404. 左叶子之和(JavaScript)
  10. 甲骨文员工谈被裁原因;《绝地求生》停机维护;谷歌正研发折叠屏样机 | 极客头条...
  11. Pandas 中文API文档
  12. 小米8 Goole Play 商店登录问题 | 正确爬山方式
  13. oracle 查历史数据,Oracle 查询历史数据(转帖)
  14. Office 2013 Excel 转换 Word
  15. Unity 彩色打印日志信息
  16. 苹果迄今最潮的产品:AirPods Max竟然有125种配色哦!
  17. 【Java基础】JDK9 模块化
  18. 【初等数论】整除、公约数、同余与剩余系
  19. ROC指标应对震荡市场
  20. 2021年全球与中国重型卡车行业市场规模及发展前景分析

热门文章

  1. Unity3D制作3D虚拟漫游场景(一)
  2. 流氓软件卸载与避免的一些方法
  3. php是世界上最好的语言滑稽,比较滑稽的句子_搞笑逗比的说说语录
  4. 嵌入式linux开发板使用pulseaudio连接蓝牙耳机播放音频文件
  5. Arduino串口控制DY-SV5W音频播放
  6. ARM 开发板嵌入式linux系统与主机PC通过串口传输文件
  7. kali系统升级(包含软件信息、所有软件、整个系统)
  8. Vmware虚拟机下三种网络模式配置
  9. 【微信小程序】别踩白块源码免费分享
  10. 小张张带你学习css美化网页,让你在学习的道路上不再孤单