小心 fastjson 的这种“智能”

最近碰到一个现象或者说问题,同一个 JSON 格式的字符串,Spring 默认的 Jackson 类库解析报错,fastjson 却没报错、正常解析了。

场景大概是这样的,有个类有个日期属性,格式指定为 “yyyy-MM-dd”。

@Data
static class Person {
    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8") // Jackson
    @JSONField(format = "yyyy-MM-dd") // fastjson
    Date birthDay;

    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
    @JSONField(format = "yyyy-MM-dd")
    Date today;

    String name;
}

测试代码如下

public void testFastJson() throws JsonProcessingException {
    String json = "{\"birthDay\":\"2022710\", \"name\": \"coderbee\", \"today\":\"2022-07-10\"}";
    Person person = JSONObject.parseObject(json, Person.class);
    System.out.println(person);     // 输出解析到对象
    System.out.println(JSONObject.toJSONString(person)); // 把对象转换为 JSON 字符串,再输出。

    ObjectMapper mapper = new ObjectMapper();
    Person jacksonPerson = mapper.readValue(json, Person.class);
    System.out.println(jacksonPerson);
}

继续阅读

Druid 与 HikariCP 获取连接的区别

在之前的文章《踩坑 Druid 连接池》说踩了坑,后面经人提醒,发现根因是一个等待获取连接的 Job 线程被终止了,通过直接调用线程的 stop 方法终止的,这种方式破坏了 ReentrantLock 锁的模型。

下面这个方法是在持有锁的情况下执行的,执行到 1491 行时,job 线程会把自己加入条件对象的等待队列、然后释放锁,等待其他线程来唤醒;

其他线程调用 notEmpty.signal() 方法时,会把 job 线程从条件对象的等待队列转移到 AQS 的获取队列上,让 job 线程重新获取锁、继续执行。

当上一个持有锁的线程释放锁后,它会唤醒下一个,即执行 662 行。

继续阅读

聊个线程有关的

最近看到逻辑类似下面的代码:

乍一看,我觉得那段异步执行的代码是没法正确把 userId 保存进数据库的,查了数据发现保存的没有问题。呃,有点意思了,为啥没有问题呢。。。

看了 UserUtil 的源码、线程池 executor 实例的初始参数、以及这个接口的请求频率后,想明白了为什么没有踩坑。
但坑是在的,一个坑没有踩中不代表不存在,可以想想请求频率什么样的时候,这个逻辑就会踩坑呢。

又踩坑了。ThreadPoolExecutor?

问题是出现在 24 号的时候,当时有台 weblogic 实例出现阻塞,运维 dump 线程栈后重启了,有个同事进行分析。

该同事分析线程栈后认为问题出在一个被外部系统调用的接口,这个接口收到请求后会从数据库查询数据,然后把数据处理后发提交到线程池,再由线程池异步发送到 MQ 服务器,调用方监听 MQ 进行数据处理,接口代码大致如下:

继续阅读

踩坑 Druid 连接池

这周有个应用的一个实例出现了没有响应,庆幸运维那边在重启前做了线程和内存的 dump 。

线程 dump 文件打开一看,竟然4万多行。。后来发现同事用一个可视化工具来分析线程栈,我也把这个工具加入工具箱:IBM Thread and Monitor Dump Analyzer for Java

下图是这个工具的方法栈分析视图:

可以按线程名词、状态、方法栈的深度来进行排序。

下面说说这次踩的坑。

继续阅读

HashMap

本文源码基于 JDK 1.8.0_101 。

在 JDK 1.8 之前,HashMap 采用 槽数组 + 单链表 来实现;

在 JDK 1.8 开始采用 槽数组 + 单链表 + 红黑树 来实现。

1. 为什么引入红黑树?

解决哈希冲突时、链表过长导致访问效率低下的问题。

为什么是红黑树不是其他树:
二叉排序树在极端情况下会退化成线性结构。
平衡二叉树(AVL树)是严格平衡树,在增加或删除节点时,旋转次数比红黑树要多。红黑树的统计性能高于 AVL 树。

红黑树特性,RBT树上的每个节点,都要遵循下面的规则:
① 每个节点都是红色或者黑色;
② 根节点必须始终是黑色;
③ 没有两个相邻的红色节点;
④ 对每个结点,从该结点到其子孙节点的所有路径上包含相同数目的黑结点。

继续阅读

HashTable 有什么奇怪的知识?

本文涉及的源码基于 JDK 1.8.0_101 。

1. HashTable

  1. 采用数组 + 链表(表头插入)方式解决哈希冲突。
  2. 所有的 public 方法都用 synchronized 修饰以保证线程安全。
  3. 在构造时就初始化槽数组(默认大小为 11)。
  4. 键、值 都不能为 null
  5. 指定 key 的目标槽的定位逻辑:(key.hashCode() & 0x7FFFFFFF) % table.length,掩码+求模。
  6. 槽数组的最大尺寸为 MAX_ARRAY_SIZE: Integer.MAX_VALUE - 8(减 8 是因为一些 JVM 会在数组里保留一些 header words)。
  7. 扩容逻辑为 两倍 + 1。
  8. 阈值为: (int)(capacity * loadFactor),但不能超过 MAX_ARRAY_SIZE + 1

继续阅读

Spring-MVC 文件上传优化

Spring-MVC 文件上传优化

  1. 配置 CommonsMultipartResolver 时把 maxInMemorySize 配置为合适的大小,让小文件可以缓存在内存中,对磁盘只需一次写操作;

  2. 在 Controller 里调用 multipartFile.transferTo(file); 把文件保存到目标路径。该方法首先进行重命名,如果不成功则进行流拷贝,如果成功则可以省下一次读、写操作。对于 org.apache.commons.fileupload.disk.DiskFileItem 类,调用的方法是下面这个:

public void write(File file) throws Exception {
    if (isInMemory()) {
        FileOutputStream fout = null;
        try {
            fout = new FileOutputStream(file);
            fout.write(get());
        } finally {
            if (fout != null) {
                fout.close();
            }
        }
    } else {
        File outputFile = getStoreLocation();
        if (outputFile != null) {
            // Save the length of the file
            size = outputFile.length();
            /*
             * The uploaded file is being stored on disk
             * in a temporary location so move it to the
             * desired file.
             */
            if (!outputFile.renameTo(file)) {
                BufferedInputStream in = null;
                BufferedOutputStream out = null;
                try {
                    in = new BufferedInputStream(
                        new FileInputStream(outputFile));
                    out = new BufferedOutputStream(
                            new FileOutputStream(file));
                    IOUtils.copy(in, out);
                } finally {
                    if (in != null) {
                        try {
                            in.close();
                        } catch (IOException e) {
                            // ignore
                        }
                    }
                    if (out != null) {
                        try {
                            out.close();
                        } catch (IOException e) {
                            // ignore
                        }
                    }
                }
            }
        } else {
            /*
             * For whatever reason we cannot write the
             * file to disk.
             */
            throw new FileUploadException(
                "Cannot write uploaded file to disk!");
        }
    }
}

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

java.util.Collections.singleton*

今天在抄 Motan 的代码时才发现 java.util.Collections 有三个以 singleton 开头的方法:

  • public static <T> List<T> singletonList(T o):返回一个内部类 SingletonList 的实例。

  • public static <T> Set<T> singleton(T o):返回一个内部类 SingletonSet 的实例。

  • public static <K,V> Map<K,V> singletonMap(K key, V value):返回一个内部类 SingletonMap 的实例。

这三个内部类都是非常高效的:
* SingletonListSingletonSet 都用一个属性来表示拥有的元素,而不是用数组、列表来表示;SingletonMap 分别用两个属性表示 key/value;内存使用上更高效;
* 在方法的实现上也更高效,减少了循环。比如 size 方法都是直接返回 1 ;List.contains 方法是把参数与属性元素直接对比。

真是一种追求性能极限的精神!我们要充分利用好这些特性。


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

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 捕获后发现满足上面的条件就会进行重试。服务端就会收到第二个请求。

继续阅读