HttpURLConnection 自动 重复 提交 POST

一、问题

测试环境偶尔反馈有个服务 HTTP 请求重复提交。这个 HTTP 请求头里有个 UUID 作为唯一标识,会存入日志表里作为主键的,因为主键冲突,服务端是有打印异常信息的,但客户端完全正常,没有任何错误信息,业务逻辑也正常完成了。

通过查看客户端、服务端两边的日志,怀疑是 HttpURLConnection 自动进行了请求重试。在网上搜索一番,发现已经有人提了 bug JDK-6427251 ,按照这个的操作步骤是可以复现的,包括 JDK 8 的版本。

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

关键是下面这段发送请求、解析响应头的方法:

  569       /** Parse the first line of the HTTP request.  It usually looks
  570           something like: "HTTP/1.0 <number> comment\r\n". */
  571   
  572       public boolean parseHTTP(MessageHeader responses, ProgressSource pi, HttpURLConnection httpuc)
  573       throws IOException {
  574           /* If "HTTP/*" is found in the beginning, return true.  Let
  575            * HttpURLConnection parse the mime header itself.
  576            *
  577            * If this isn't valid HTTP, then we don't try to parse a header
  578            * out of the beginning of the response into the responses,
  579            * and instead just queue up the output stream to it's very beginning.
  580            * This seems most reasonable, and is what the NN browser does.
  581            */
  582   
  583           try {
  584               serverInput = serverSocket.getInputStream();
  585               if (capture != null) {
  586                   serverInput = new HttpCaptureInputStream(serverInput, capture);
  587               }
  588               serverInput = new BufferedInputStream(serverInput);
  589               return (parseHTTPHeader(responses, pi, httpuc));
  590           } catch (SocketTimeoutException stex) {
  591               // We don't want to retry the request when the app. sets a timeout
  592               // but don't close the server if timeout while waiting for 100-continue
  593               if (ignoreContinue) {
  594                   closeServer();
  595               }
  596               throw stex;
  597           } catch (IOException e) {
  598               closeServer();
  599               cachedHttpClient = false;
  600               if (!failedOnce && requests != null) {
  601                   failedOnce = true;
  602                   if (httpuc.getRequestMethod().equals("POST") && (!retryPostProp || streaming)) {
  603                       // do not retry the request
  604                   }  else {
  605                       // try once more
  606                       openServer();
  607                       if (needsTunneling()) {
  608                           httpuc.doTunneling();
  609                       }
  610                       afterConnect();
  611                       writeRequests(requests, poster);
  612                       return parseHTTP(responses, pi, httpuc);
  613                   }
  614               }
  615               throw e;
  616           }
  617   
  618       }

在第 600 – 614 行的代码里:

  • failedOnce 默认是 false,表示是否已经失败过一次了。这也就限制了最多发送 2 次请求。
  • httpuc 是请求相关的信息。
  • retryPostProp 默认是 true,可以通过命令行参数(-Dsun.net.http.retryPost=false)来指定值。
  • streaming:默认 falsetrue if we are in streaming mode (fixed length or chunked) 。

通过 Linux 的命令 socat tcp4-listen:8080,fork,reuseaddr system:"sleep 1"\!\!stdout 建立一个只接收请求、不返回响应的 HTTP 服务器。
对于 POST 请求,第一次请求发送出去后解析响应会碰到流提前结束,这是个 SocketException: Unexpected end of file from serverparseHTTP 捕获后发现满足上面的条件就会进行重试。服务端就会收到第二个请求。

继续阅读

Javassist 字节码操作库

研究 Javassist 的起因是维护的项目是从外部采购的一个系统,有几个核心的类在构造函数里从数据库加载一些配置信息,而这些配置信息基本是不会改变的,应当缓存起来。这些类没有源码,class 文件还是混淆过的。

想来想去,觉得用字节码操作工具来改写是比较合适的,把改写后生成的 class 文件替换原来的,这样使用这些类的地方也不用做任何修改。

1. Javassist 简介

Javassist 是一个字节码操作库,通过它,可以在运行时改写类:添加新的字段、方法和构造函数,改变类、父类和接口的方法。

Javassist 定义了 CtField, CtMethod, CtConstructor, CtClass 来表示 字段、方法、构造函数、类。

继续阅读

Thread, Runnable, Callable

最近观察同事面试,发现他对问题的理解本身就有不对,这样即使面试者完全掌握了那个问题,也可能给不了他想要的回答。

他的问题是:Java 里有哪些创建线程的方式?他期望的答案是 Thread, Runnable, Callable, Future 这些。

这个理解是错误的,他也是网上看的。

一、线程与任务

线程可以理解为 可以执行程序指令的机器,而任务则是要执行的一段指令

在 Java 里用 java.lang.Thread 类来表示线程。用 java.lang.Runnable 接口表示任务。

任务创建出来后是需要放到线程上去执行的,所谓线程驱动任务执行

创建、启动线程

创建线程一般是创建一个 Thread 类的实例,可以在创建时指定任务,也可以覆写 void run() 方法来实现任务的逻辑。

Thread 类实例化后只是在 Java 堆里创建了一个线程对象,并没有跟操作系统的线程关联起来,还不能真正执行任务。需要调用 native void start() 方法(这个方法在不同的 JDK 版本里有所不同)。所以如果是继承了 Thread 类,是不能覆写 start 方法的,否则是没法启动线程的。

start 方法调用后,线程会自动执行 run 方法, run 方法执行完成后线程就会销毁。

二、Callable, Future

java.util.concurrent.Callable 表示一个带返回值的任务,是一种特殊点任务。

java.util.concurrent.Future 表示一个可在未来获得调用结果的存根。


欢迎关注我的微信公众号: coderbee笔记,可以更及时回复你的讨论。

发布组件到 Maven 中央仓库

折腾了几天,终于把一个小组件MyBatis-batch 发布了中央仓库,做个笔记记录下。

  1. 注册 sonatype JIRA 帐号并配置 settings.xml
    我是先在 sonatype 上发布,然后由 sonatype 自动同步到中央仓库的。首先要在 sonatype 注册一个 JIRA 帐号,

$M2_HOME/conf/settings.xmlservers 标签下添加如下配置:

<server>
    <id>sonatype-nexus</id>
    <username>sonatype 登录名</username>
    <password>sonatype 密码</password>
</server>
  1. 在 github 添加 ssh key
    Maven 构建的时候,会自动操作 github,比如创建 tag 。
    用 ssh-keygen 生成一对秘钥,在 https://github.com/settings/keys 页面可以添加 SSH key。

继续阅读

Java finalize 方法与垃圾回收

JVM 对于覆写了 Object.finalize() 方法的类是实例,在垃圾回收时,先放进一个队列里,
然后用一个线程执行这些实例的 finalize 方法,最后才做内存回收。

这个机制的问题是:它会影响垃圾回收,如果实现了 finalize 的实例很多,那么回收队列就会很长,而只有一个线程
在执行这些方法,这些对象就会回收得很慢。

最近碰到的一个问题跟 finalize 方法有关。
厂商提供的代码把 javax.sql.Connection 做了层封装,覆写了 finalize 方法,在里面把 Connection 关闭了。

一个同事调用的方法定义是用封装类作为入参,他调用的时候是直接 new 了一个封装类,把 connection 传进去,后续数据库操作就不定位置出现连接关闭的异常。原因就是这个临时 new 出来的封装类被垃圾回收,把 connection 关闭了。由于垃圾回收的时间是不确定的,所以报异常的时间点也不确定。


欢迎关注我的微信公众号: coderbee笔记,可以更及时回复你的讨论。

接口与Spring自动注入

接口与自动注入

有业务接口 IService 和两种业务逻辑的实现 Aservice, Bservice 如下:

public interface IService {}

@Component
public class Aservice implements IService {}

@Component
public class Bservice implements IService {}

采用下面的方式自动注入时,会报错:

public class ManageService {
    @Autowired
    private Aservice aservice;

    @Autowired
    private Bservice bservice;

    // ...
}

异常信息类似为:nested exception is java.lang.IllegalArgumentException: Can not set xxx.ManageService field xxx.ManageService.aservice to com.sun.proxy.$Proxy48

继续阅读

Object Extract Mapping 与 网页 API

最近的项目需要访问一些外部的网页,比如百度、必应搜索等,有的只是要求把响应内容的文本提取出来,有的是要求做结构化解析的,比如百度的搜索结果,解析为一个结构化的列表,每个列表项有(标题、概要信息、详细信息的链接)。

开始时感觉是用爬虫去爬网页,然后做解析,但有个问题困扰着:比如要保存百度、必应的搜索结果到数据库,肯定只希望定义一个类来对应搜索结果的一条记录,然后写一套保存到数据库的代码。看了一些爬虫框架对于内容解析部分其实都只能自己写代码来解析网页,这样的话,如果要接搜狗就写一套解析网页的逻辑、接 360 也得写一套。。。。每接一个就得写一套。。。这不是我需要的生活。。。

怎么减少这些匹配代码?

后面看到 webmagic 的文档提到其基于注解实现的 Object/Extraction Mapping 功能,在类的字段上定义一个注解,表示这个字段用什么样的方式抽取,这样在爬虫有关的代码里就不会出现这些解析网页的逻辑。

Object Extract Mapping

如果我只需要解析百度的搜索结果,这是很好的方式,但我还要解析必应、搜狗等更多的搜索结果时就不行了。因此需要换一种方式来实现:把抽取方式的定义放到代码外面来

我就实现了一个基于 XML 的 Object-Extrac-Mapping (OEM) 小工具。

继续阅读

Spring 导入资源文件

Spring 有多种方式可以导入资源文件,可以把这些属性文件里的属性值注入到 bean 的属性上。

1. 通过 <context:property-placeholder> 标签导入

通过这种方式导入的所有属性在同一个命名空间下,如果多个属性文件里有相同的属性名,以先导入的属性文件的属性为准,不会出现覆盖。可以通过 @Value("${propName}") 的方式注入到 bean 的属性上。

这种方式不能对属性文件指定 id,没法引用指定属性文件的属性,如果有同名的属性,引用到的总是第一个导入的属性。

配置如下:

<!-- 导入外部的资源文件,可以通过 @Value("${propName}") 的方式注入到 bean 的属性上 。 -->
<context:property-placeholder
    location="classpath:/learn/properties/1.properties"
    ignore-unresolvable="true" />
<!-- 要使用 context:property-placeholder 导入多个资源文件,必须指定 ignore-unresolvable="true" -->
<!-- 多个属性文件中,属性名相同的,以第一个加载的为准,不会出现后面加载的覆盖前面的 -->
<context:property-placeholder location="learn/properties/2.properties"
    ignore-unresolvable="true" />

继续阅读

基于POI检查 excel 文件的行数、列数是否超出限制

用 POI 解析 xlsx 时,如果用这样的方式 Workbook workbook = WorkbookFactory.create(new File("super-many-row.xlsx")); 解析具有超过 100 万行的 excel ,很可能内存就被耗尽了。

因为这样虽然简单,但是要等整个 excel 都解析完成、转换成 POI 定义的对象后才会返回到业务代码,这时候才判断行数是否超过限制就晚了。

需要一种更高效的方式,在解析完成前就判断是否超过限制,了解了 POI 的 API 和 xlsx 的基本格式后,就有了下面这个工具类,基于 POI 3.13。

2016.02.28 更新:增加对 sheet 数量的校验才够完整。

继续阅读

隐式类型转换导致全表扫描

最近系统改版了一部分(其实就是重做。。。),上线之后又开始出现数据库 IO 告警了。

DBA 抓出一个全表扫描的 SQL,是把范围在两个钟内的数据找出来做更新的,这个查询的时间字段是有索引的,结果却没有用上,
跟另一个表关联的时候,关联字段也是有索引的,但还是没有用上。

在两个钟内的数据量应该是很小的,相对于总量来说,时间字段的索引没理由不用啊;从主表找出来的数据量不大,再去关联表查询也应该走索引的。
感觉就是执行计划不对了,去找 DBA 做固化。

找 DBA 先用 hint 生成一个走索引的执行计划,然后做了固化。

这个 SQL 是两个钟才执行一次的,结果固化之后值班 DBA 说主表还是全表扫描。找 DBA 再做一次固化,还是主表全表扫描。最后开了 tunning ,提示说有隐式类型转换,主表没法走索引。

大致扫了一下代码,Java 里用的数据类型是 Date 的,而表的列的类型也是 Date,应该不需要做隐式类型转换啊。

问题出在 MyBatis 的参数类型指定为 timestamp 而不是 Date;由于查询需要时分秒,而 Date 会抹去这些值,所以被指定为 timestamp,
而这个类型刚好对应到 Oracle 的 timestamp 类型,所以就出现隐式类型转换。

细节吶。。。


欢迎关注我的微信公众号: coderbee笔记,可以更及时回复你的讨论。