高性能格式化LocalDateTime代码分享
文章讨论了LocalDateTime.parse方法在高频调用时性能较差的问题,原因是其内部包含大量解析和合法性校验逻辑。通过Benchmark测试,发现使用LocalDateTime.of、MethodHandler和反射创建LocalDateTime实例的性能差异显著,其中MethodHandler性能最佳。最终,作者提出并实现了一个自定义的FastDateTimeFormatter,通过预解析时间格式串来提升性能,测试结果显示其解析速度比LocalDateTime.parse快15倍。
文章目录
起因
起因是在技术群内有人在排查某个高频方法耗时时,发现通过LocalDateTime.parse方法解析时间字符串消耗了大量时间,进一步查看源码,可以看到该方法做了大量的解析逻辑和合法判断,以支持大量占位符同时保证时间合法。然而在大多数业务场景下,时间格式都是固定的,常用的格式有ISO-8601(yyyy-MM-ddTHH:mm:ss.SSS)或者空格分割(yyyy-MM-dd HH:mm:ss)等,并不会用到其他更多的占位符格式,并且格式一旦确定下来后期基本不会更改,因此LocalDateTime.parse的实现显得比较“重”,性能不高。
经过大家讨论,解决方案基本都选择了自行解析时间字符串,并创建LocalDateTime实例这一方式,主要讨论的点在LocalDateTime创建方式上,LocalDateTime的构造方法不包含任何逻辑判断,但是该构造方法是私有的,正常情况下只能通过静态方法LocalDateTime.of(int, int, int, int, int, int, int)来创建,而这个of方法中包含了时间合法性的校验,性能不够极致(。
于是我写了一个Benchmark测试,分别测试了通过LocalDateTime.of、MethodHandler获取构造方法、反射获取构造方法 这3种方式下的性能(由于Unsafe构造对象的相关方法在未来会被移除,故不考虑该方式),代码如下(基于OpenJDK 22.0.1,JMH 1.3.7):
package com.example;
import com.example.util.DateTimeUtil;import org.junit.jupiter.api.Assertions;import org.openjdk.jmh.annotations.*;import org.openjdk.jmh.runner.Runner;import org.openjdk.jmh.runner.RunnerException;import org.openjdk.jmh.runner.options.Options;import org.openjdk.jmh.runner.options.OptionsBuilder;
import java.time.LocalDateTime;import java.util.concurrent.TimeUnit;
@State(Scope.Thread)@BenchmarkMode(Mode.Throughput)@Warmup(iterations = 100, time = 100, timeUnit = TimeUnit.MILLISECONDS)@Measurement(iterations = 100, time = 100, timeUnit = TimeUnit.MILLISECONDS)@Threads(8)@OutputTimeUnit(TimeUnit.MILLISECONDS)public class FastParseBenchTest { private static final LocalDateTime now = LocalDateTime.now();
// 原生of静态方法,带时间校验 @Benchmark public void test_LocalDateTime_of() { LocalDateTime t = LocalDateTime.of(1970, 1, 1, 0, 0, 0, 0); Assertions.assertTrue(t.isBefore(now)); }
// 方法句柄获取构造器,不带时间校验 @Benchmark public void test_MethodHandler() throws Throwable { LocalDateTime t = DateTimeUtil.methodHandler(1970, 1, 1, 0, 0, 0, 0); Assertions.assertTrue(t.isBefore(now)); }
// 反射获取构造器,不带时间校验 @Benchmark public void test_Reflect() throws Throwable { LocalDateTime t = DateTimeUtil.reflect(1970, 1, 1, 0, 0, 0, 0); Assertions.assertTrue(t.isBefore(now)); }
public static void main(String[] args) throws RunnerException { Options opt = new OptionsBuilder() .include(FastParseBenchTest.class.getSimpleName()) .output("./construct_LocalDateTime.log") .forks(2) .build();
new Runner(opt).run(); }}package com.example.util;
import java.lang.invoke.MethodHandle;import java.lang.invoke.MethodHandles;import java.lang.invoke.MethodType;import java.lang.reflect.Constructor;import java.time.LocalDate;import java.time.LocalDateTime;import java.time.LocalTime;
public class DateTimeUtil { static final MethodHandle DATE_CONSTRUCTOR; static final MethodHandle TIME_CONSTRUCTOR; static final Constructor<LocalDate> DATE_CONSTRUCTOR2; static final Constructor<LocalTime> TIME_CONSTRUCTOR2;
static { try { var lookup = MethodHandles.privateLookupIn(LocalDate.class, MethodHandles.lookup()); DATE_CONSTRUCTOR = lookup.findConstructor(LocalDate.class, MethodType.methodType(void.class, int.class, int.class, int.class));
lookup = MethodHandles.privateLookupIn(LocalTime.class, MethodHandles.lookup()); TIME_CONSTRUCTOR = lookup.findConstructor(LocalTime.class, MethodType.methodType(void.class, int.class, int.class, int.class, int.class));
DATE_CONSTRUCTOR2 = LocalDate.class.getDeclaredConstructor(int.class, int.class, int.class); DATE_CONSTRUCTOR2.setAccessible(true); TIME_CONSTRUCTOR2 = LocalTime.class.getDeclaredConstructor(int.class, int.class, int.class, int.class); TIME_CONSTRUCTOR2.setAccessible(true); } catch (Exception e) { throw new RuntimeException(e); } }
public static LocalDateTime methodHandler(int year, int month, int day, int hour, int minute, int second, int milli) throws Throwable { return LocalDateTime.of( (LocalDate) DATE_CONSTRUCTOR.invoke(year, month, day), (LocalTime) TIME_CONSTRUCTOR.invoke(hour, minute, second, milli) ); }
public static LocalDateTime reflect(int year, int month, int day, int hour, int minute, int second, int milli) throws Throwable { return LocalDateTime.of( DATE_CONSTRUCTOR2.newInstance(year, month, day), TIME_CONSTRUCTOR2.newInstance(hour, minute, second, milli) ); }}Benchmark结果如下(仅供参考):
Benchmark Mode Cnt Score Error UnitsFastParseBenchTest.test_LocalDateTime_of thrpt 200 2688555.251 ± 42472.191 ops/msFastParseBenchTest.test_MethodHandler thrpt 200 8641022.507 ± 149300.689 ops/msFastParseBenchTest.test_Reflect thrpt 200 6624552.853 ± 63160.076 ops/ms结果真的让人大跌眼镜啊,LocalDateTime.of性能最差,不到反射的1/2,而方法句柄性能最佳,最接近原生构造器。经过测试可以得知,时间校验是造成解析耗时长的原因之一,但为了安全性,校验逻辑应当保留,并且使用方法句柄和反射获取标准库私有构造器需要在VM参数中加上--add-opens java.base/java.time=ALL-UNNAMED,所以只能从别的地方下手了。
自行实现解析逻辑
既然时间校验应当保留,那么我们只能从解析阶段下手,上面提到业务中时间格式一般定下来之后就不会更改,所以我们可以自己写一个DateTimeFormatter,实现构建DateTimeFormatter时解析一次时间格式串,后续parse/format不用再解析的效果。整体流程如下图:

最后代码如下,使用方式和java.time.DateTimeFormatter一致,但仅支持LocalDateTime、LocalDate和LocalTime:
import java.time.LocalDate;import java.time.LocalDateTime;import java.time.LocalTime;import java.time.temporal.ChronoField;import java.time.temporal.TemporalAccessor;import java.util.ArrayList;import java.util.Arrays;import java.util.List;import java.util.Map;import java.util.stream.Collectors;
/** * 快速时间格式化工具,支持时间字符串解析和格式化 * 支持的pattern格式有 * <table class="striped"> * <thead> * <tr> * <th scope="col">符号</th> <th scope="col">含义</th> <th scope="col">示例</th> * </tr> * </thead> * <tbody> * <tr> * <th>yyyy</th> <td>年份</td> <td>1970</td> * </tr> * <th>MM</th> <td>月份</td> <td>01</td> * </tr> * </tr> * <th>dd</th> <td>日期</td> <td>02</td> * </tr> * </tr> * <th>HH</th> <td>时</td> <td>03</td> * </tr> * </tr> * <th>mm</th> <td>分</td> <td>04</td> * </tr> * </tr> * <th>ss</th> <td>秒</td> <td>05</td> * </tr> * </tr> * <th>SSS</th> <td>毫秒数</td> <td>789</td> * </tr> * </tbody> * </table> * * @author brooke_zb * @since 2024/6/17 */public class FastDateTimeFormatter { private final Node[] nodes;
private static final Map<Character, Integer> PATTERN_INDEX = Arrays.stream(NodeType.values()) .filter(e -> e != NodeType.TEXT) .collect(Collectors.toMap(e -> e.pattern.charAt(0), Enum::ordinal));
private FastDateTimeFormatter(String pattern) { int[] count = new int[7]; // y, M, d, H, m, s, S List<MutableNode> nodes = new ArrayList<>();
int start = 0; char last = 0; for (int i = 0; i < pattern.length(); i++) { char c = pattern.charAt(i); if (PATTERN_INDEX.containsKey(c)) { int index = PATTERN_INDEX.get(c); count[index]++; NodeType type = NodeType.values()[index]; if (type.length == count[index]) { addNode(nodes, NodeType.values()[index], null); clearExcept(count, index); start = i + 1; } else { if (last != c || i == pattern.length() - 1) { addNode(nodes, NodeType.TEXT, pattern.substring(start, i)); start = i; } } } else { addNode(nodes, NodeType.TEXT, pattern.substring(start, i + 1)); clearExcept(count, -1); start = i + 1; } last = c; }
this.nodes = nodes.stream().map(MutableNode::toNode).toArray(Node[]::new); }
private static void addNode(List<MutableNode> nodes, NodeType type, String value) { if (type == NodeType.TEXT) { if (value.isEmpty()) { return; } if (nodes.isEmpty() || nodes.get(nodes.size() - 1).type != NodeType.TEXT) { nodes.add(new MutableNode(type, value)); return; } nodes.get(nodes.size() - 1).append(value); return; } nodes.add(new MutableNode(type, value)); }
@Override public String toString() { StringBuilder sb = new StringBuilder(); for (Node node : nodes) { if (node.type == NodeType.TEXT) { sb.append(node.value); } else { sb.append(node.type.pattern); } } return sb.toString(); }
/** * 从格式串创建FastDateTimeFormatter实例 * 支持的格式有yyyy, MM, dd, HH, mm, ss, SSS */ public static FastDateTimeFormatter ofPattern(String pattern) { if (pattern == null || pattern.isEmpty()) { throw new IllegalArgumentException("Invalid pattern: " + pattern); } return new FastDateTimeFormatter(pattern); }
/** * 将时间字符串解析为LocalDateTime */ public LocalDateTime parse(CharSequence text) { int[] values = parseDateTimeValues(text); return LocalDateTime.of(values[0], values[1], values[2], values[3], values[4], values[5], values[6] * 1000000); }
/** * 将时间字符串解析为LocalDate */ public LocalDate parseDate(CharSequence text) { int[] values = parseDateTimeValues(text); return LocalDate.of(values[0], values[1], values[2]); }
/** * 将时间字符串解析为LocalTime */ public LocalTime parseTime(CharSequence text) { int[] values = parseDateTimeValues(text); return LocalTime.of(values[3], values[4], values[5], values[6] * 1000000); }
private int[] parseDateTimeValues(CharSequence text) { int[] values = new int[7]; int start = 0; for (Node node : nodes) { if (node.type == NodeType.TEXT) { start += node.value.length(); } else { int end = start + node.type.length;
int val = 0; for (int i = start; i < end; i++) { val = val * 10 + (text.charAt(i) - '0'); } values[node.type.ordinal()] = val; start = end; } } return values; }
/** * 格式化为字符串 */ public String format(TemporalAccessor dateTime) { StringBuilder sb = new StringBuilder(); for (Node node : nodes) { if (node.type == NodeType.TEXT) { sb.append(node.value); } else { switch (node.type) { case YEAR: sb.append(toAlignNumber(dateTime.get(ChronoField.YEAR), 4)); break; case MONTH: sb.append(toAlignNumber(dateTime.get(ChronoField.MONTH_OF_YEAR), 2)); break; case DAY: sb.append(toAlignNumber(dateTime.get(ChronoField.DAY_OF_MONTH), 2)); break; case HOUR: sb.append(toAlignNumber(dateTime.get(ChronoField.HOUR_OF_DAY), 2)); break; case MINUTE: sb.append(toAlignNumber(dateTime.get(ChronoField.MINUTE_OF_HOUR), 2)); break; case SECOND: sb.append(toAlignNumber(dateTime.get(ChronoField.SECOND_OF_MINUTE), 2)); break; case MILLISECOND: sb.append(toAlignNumber(dateTime.get(ChronoField.MILLI_OF_SECOND), 3)); break; } } } return sb.toString(); }
private String toAlignNumber(int value, int length) { String s = String.valueOf(value); if (s.length() >= length) { return s; } return "0".repeat(length - s.length()) + s; }
private void clearExcept(int[] count, int exceptIndex) { for (int i = 0; i < count.length; i++) { if (i != exceptIndex) { count[i] = 0; } } }
private static final class Node { final NodeType type; final String value;
Node(NodeType type, String value) { this.type = type; this.value = value; } }
private static class MutableNode { NodeType type; StringBuilder value;
MutableNode(NodeType type, String value) { this.type = type; if (type == NodeType.TEXT) { this.value = new StringBuilder(value); } }
void append(String s) { value.append(s); }
Node toNode() { if (type == NodeType.TEXT) { return new Node(type, value.toString()); } return new Node(type, null); } }
private enum NodeType { YEAR("yyyy", 4), MONTH("MM", 2), DAY("dd", 2), HOUR("HH", 2), MINUTE("mm", 2), SECOND("ss", 2), MILLISECOND("SSS", 3), TEXT("", 0), ;
public final String pattern; public final int length;
NodeType(String pattern, int length) { this.pattern = pattern; this.length = length; } }}附上一个Benchmark结果,解析时间字符串场景下比LocalDateTime.parse要快上15倍左右(仅供参考):
Benchmark Mode Cnt Score Error UnitsDateTimeUtilTest.testFastDateTimeFormatter_parse thrpt 200 90359.275 ± 557.471 ops/msDateTimeUtilTest.testLocalDateTime_parse thrpt 200 6018.009 ± 39.635 ops/ms