HttpURLConnection Post请求自动重传机制

背景故事

之前负责的一个商城项目,需要从供应商库进行订单下单同步,服务器间通讯通过http请求。 加密方式采用DES加密方式。在运行初期一切正常,几个月后
供应商发现有重复订单存在,而客户端这边接收到异常生成订单异常信息,订单生成不同步。供应商的处理逻辑我们无从得知,只能从自身角度思考为什么会有这
种问题,在排除了一系列原因后,定位到一个问题。那就是 HttpURLConnection的post请求重发机制。

场景再现

Http请求是通过HttpUrlConnection封装的一套Java请求客户端 部分源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
 //请求的header如下
protected Map<String, Object> getDefaultHeaders() {
Map<String, Object> defaultHeaders = new HashMap<>();
defaultHeaders.put("Accept", "*/*");
defaultHeaders.put("Connection", "Keep-Alive");
defaultHeaders.put("User-Agent", "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1)");
defaultHeaders.put("Accept-Charset", "utf-8");
defaultHeaders.put("Content-Type", "application/x-www-form-urlencoded;charset=utf-8");
headers = defaultHeaders;
return headers;
}

//初始化 httpConnection
protected void initConnection() throws IOException {
if ("POST".equals(method)) {
this.url = new URL(getUrl);
this.postJson = JsonUtil.toJson(param);
} else {
this.url = new URL(getUrl + urlParams);
}
httpConnection = (HttpURLConnection) url.openConnection();
for (String keyset : headers.keySet()) {
httpConnection.setRequestProperty(keyset, headers.get(keyset).toString());
}
/**
* 然后把连接设为输出模式。URLConnection通常作为输入来使用,比如下载一个Web页。
* 通过把URLConnection设为输出,你可以把数据向你个Web页传送。:
*/
httpConnection.setRequestMethod(method);
httpConnection.setUseCaches(false);
if ("POST".equals(method)) {
httpConnection.setDoOutput(true);
} else {
httpConnection.setDoOutput(true);
}
httpConnection.setDoInput(true);
}

//执行Http请求
public String doRequest() {
this.toUrlParams();
OutputStreamWriter out = null;
try {
this.initConnection();
// 一旦发送成功,用以下方法就可以得到服务器的回应:
String sTotalString;
InputStream urlStream;
out = new OutputStreamWriter(httpConnection.getOutputStream(), charSet);
if (method.equals("POST")) {
out.write(this.postJson); //向页面传递数据。post的关键所在!
}
// remember to clean up
out.flush();
urlStream = httpConnection.getInputStream();
logger.debug("连接状态:" + urlStream.available());
//new InputStreamReader(l_urlStream,)
sTotalString = IOUtil.in2Str(urlStream, charSet);
return sTotalString;
} catch (Exception e) {
e.printStackTrace();
throw new SystemException(e);
} finally {
if (out != null) {
try {
out.close();
} catch (Exception e) {
e.printStackTrace();
throw new SystemException(e);
}
}
httpConnection.disconnect();
}
}

Java代码调用doRequest通过HttpUrlConnection模拟一个Post请求。结果服务端会收到两次请求。

原因分析

HttpURLConnection 采用 Sun 私有的一个 HTTP 协议实现类: HttpClient.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public boolean parseHTTP(MessageHeader var1, ProgressSource var2, HttpURLConnection var3) throws IOException {
try {
this.serverInput = this.serverSocket.getInputStream();
if(this.capture != null) {
this.serverInput = new HttpCaptureInputStream(this.serverInput, this.capture);
}

this.serverInput = new BufferedInputStream(this.serverInput);
return this.parseHTTPHeader(var1, var2, var3);
} catch (SocketTimeoutException var6) {
if(this.ignoreContinue) {
this.closeServer();
}

throw var6;
} catch (IOException var7) {
this.closeServer();
this.cachedHttpClient = false;
if(!this.failedOnce && this.requests != null) {
this.failedOnce = true;
if(!this.getRequestMethod().equals("CONNECT") && !this.streaming && (!var3.getRequestMethod().equals("POST") || retryPostProp)) {
this.openServer();
if(this.needsTunneling()) {
MessageHeader var5 = this.requests;
var3.doTunneling();
this.requests = var5;
}

this.afterConnect();
this.writeRequests(this.requests, this.poster);
return this.parseHTTP(var1, var2, var3);
}
}
throw var7;
}
}

当发生IOException就会执行判断是否进行重试。
failedOnce 默认是 false,表示是否已经失败过一次了。这也就限制了最多发送 2 次请求。
var3 是请求信息
retryPostProp 默认是 true ,可以通过命令行参数( -Dsun.net.http.retryPost=false )来指定值。
streaming:默认 false 。 true if we are in streaming mode (fixed length or chunked) 。

bug链接:http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6427251
这个Bug很早就有了,归根结底原因就是sun提供的实现与Http对于Post请求的规范有不同。Http协议里Post不是幂等的,不能进行重试。

解决方案

  1. 使用Apache Client请求
  2. 修改JVM启动参数 添加:-Dsun.net.http.retryPost=false

http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6427251

总结心得

  • http协议方面:http规定的部分是规范,实现有千种方法。有的符合协议,有的又有所区别,在对接过程中,指定接入方式,形成书面文档规范。有利于后续
    问题职责归属。
  • 在寻找问题方面,无法完整获取所有信息时,从已掌握的信息出发,避免任何一点得出结论的依据。