Java 编码指南
面向日常 Java 开发的编码约定助手。每条规则含「✗ 禁止 → ✓ 推荐 → 为什么」。 问题优先、一域一默认、框架中立(不含 Spring)。基于 Hutool 5.8.x + 主流库(OkHttp3 / Jackson / MapStruct / Lombok / SLF4J),覆盖 JDK 8~21+。 必选依赖:Hutool(core 必需;crypto 按需)+ Lombok + OkHttp3 + Jackson + MapStruct + SLF4J(坐标见「依赖坐标」段;不含 Guava / commons-lang3)。
三条铁律(开工前先记住)
- 框架中立:所有推荐默认不依赖 Spring。本指南不出现任何 Spring 专属方案。
- 一域一默认:每个场景只给唯一推荐,不列举「或 A 或 B」制造决策疲劳。
- 版本门控:先用「JDK 版本策略」确认目标版本,高版本特性按门控使用——JDK 8 项目严禁
var/record/switch 表达式/文本块/sealed/虚拟线程。
选型哲学(三档,按场景而非按库)
| 原生 API 状态 | 选型 |
|---|---|
| 完善(设计良好、完整) | 用 JDK 原生:java.time、Optional、Stream、CompletableFuture、ThreadPoolExecutor |
| 低级易错(易踩坑) | 用工具库封装:加密→Hutool SecureUtil;字符串→StrUtil;集合→CollUtil |
| 原生缺失 | 用行业主流库:HTTP→OkHttp3;JSON→Jackson;Bean→MapStruct;BCrypt→hutool-crypto |
反面:不要因为「熟悉」就保留旧 API+workaround(如
ThreadLocal<SimpleDateFormat>),优先用更优 API(DateUtil/java.time)。
JDK 版本策略(激活时先执行)
- 探测目标 JDK:读
pom.xml的maven.compiler.release/<source>/<target>,或build.gradle的sourceCompatibility/targetCompatibility;读不到则主动问用户「目标 JDK 版本是?」。 - 三档语义:
- JDK 8(下限):可用
Optional/Stream/java.time/CompletableFuture/Lambda/方法引用。禁用var/record/switch 表达式/文本块/sealed/虚拟线程/Stream.toList()。 - JDK 11–17:+
var(10)、JDKHttpClient(11)、switch表达式(14)、文本块(15)、record(16)、sealed(17)、Stream.toList()(16)。 - JDK 21+:+ 虚拟线程、
switch模式匹配、SequencedCollection、record 模式。
- JDK 8(下限):可用
- 详细门控见
references/09-modern-java.md。
域 → 唯一默认(路由表)
| 场景信号 | 唯一默认 | 详见 |
|---|---|---|
判 null / Optional 取值 / a.equals(b) 防 NPE |
JDK Optional/Objects;Hutool StrUtil.isBlank/ObjectUtil.equal |
01 |
| 字符串判空/格式化/截取/分隔/命名转换/拼接 | Hutool StrUtil |
01 |
| 集合判空/新建/分块/交并差 | Hutool CollUtil |
02 |
| 集合分组/转Map | JDK Stream/Collectors(默认);链式组合用 CollStreamUtil |
02 |
| 日期格式化/解析/加减/差值/时间戳 | JDK java.time 优先;Hutool DateUtil 处理遗留 Date |
03 |
| 文件读写/流拷贝/遍历 | Hutool FileUtil/IoUtil |
04 |
| HTTP GET/POST 调用 | OkHttp3 | 04 |
| JSON 序列化/反序列化 | Jackson ObjectMapper |
04 |
| 线程池/异步/锁/虚拟线程 | JDK ThreadPoolExecutor + CompletableFuture |
05 |
| Bean 属性拷贝/转Map/克隆 | MapStruct(默认,编译期安全);toString 用 Lombok @ToString |
06 |
| MD5/SHA/对称加密/密码哈希 | Hutool SecureUtil/DigestUtil/BCrypt/AES(均 hutool-crypto);Base64 编解码用 hutool-core 的 Base64 |
07 |
| 随机数 | Hutool RandomUtil |
08 |
| 异常链/根因/断言 | Hutool ExceptionUtil/Assert |
08 |
| 日志记录 | SLF4J 门面 + Logback(版本按 JDK 门控) | 08 |
| 现代 Java 语法(版本门控) | 见 09-modern-java.md |
09 |
| 金额/浮点/精确小数运算 | JDK BigDecimal(禁 double/float 算钱) |
10 |
强约束(生成代码前必须满足)
- 判空:字符串→
StrUtil.isBlank;集合→CollUtil.isEmpty;禁== null || .isEmpty()/.trim().isEmpty()。 - 相等防 NPE:
ObjectUtil.equal(a,b)/StrUtil.equals(a,b)/ 常量在前;禁a.equals(b)可能 NPE。 - 日期:禁
SimpleDateFormat作共享/静态变量(线程不安全);禁Calendar手算(月从 0);用DateUtil或java.time。 - Bean 拷贝:默认 MapStruct;仅无 annotation processor / 一次性临时拷贝退
BeanUtil.copyProperties;禁随手 Spring/ApacheBeanUtils且不确认源/目标顺序。 - 加密:禁手搓
MessageDigest(前导零丢失);用SecureUtil.md5/sha256。密码禁无盐 MD5/SHA,用BCrypt.hashpw(pwd)。 - 集合新建:
CollUtil.newArrayList(a,b);禁new ArrayList<>()后逐个add仅初始化。 - 集合分块/分页:
CollUtil.partition/page;禁subList手写(越界 + 视图耦合)。 - 集合分组/转Map:默认 JDK
Collectors.groupingBy/toMap(链式组合用CollStreamUtil);禁手写for+computeIfAbsent。 - IO:
FileUtil/IoUtil;禁手搓流且close不兜底(必须 try-with-resources)。 - HTTP:OkHttp3
Request.Builder+ try-with-resources;禁手搓HttpURLConnection。 - JSON:Jackson
ObjectMapper(复用单例);禁手拼 JSON 字符串。 - 线程池:
new ThreadPoolExecutor+ 有界队列 + 明确拒绝策略;禁new Thread().start();禁Executors.newCachedThreadPool/newFixedThreadPool(无界队列 OOM 风险)。 - 日志:SLF4J 占位符
log.error("x={}, y={}", x, y);禁字符串拼接log.error("x="+e)。异常对象作最后一个参数不占位。 - 随机数:
RandomUtil.randomInt(min,max)([min,max) 半开);禁new Random().nextInt()手算范围。 - 现代语法:按目标 JDK 门控(见
09);JDK 8 项目禁var/record/switch 表达式/文本块。
借口对照表(基线测试得来,每个都要正面拦截)
| 借口(Agent 可能这么想) | 现实 |
|---|---|
| 「单个方法没必要引 Hutool,手搓 MessageDigest 一样」 | 手搓的 hex 转换极易丢前导零导致哈希碰撞;SecureUtil.md5 一行且零风险。Hutool 是团队统一标准,不是可选优化。 |
「warn 级别一直开着,字符串拼接 log.warn("x="+id) 差别可忽略」 |
拼接每次都执行(即使该日志被关闭);占位符 {} 只在真正输出时格式化。一致用占位符是团队约定,无例外。 |
| 「ThreadLocal<SimpleDateFormat> 已经解决线程安全,没必要换」 | ThreadLocal 是 workaround,有内存泄漏风险、有线程池场景复用问题。DateUtil.format/DateTimeFormatter 本身线程安全,是更优 API。 |
「Stream toMap 一行搞定,不用 Hutool 也行」 |
对的——JDK Stream 完善时用 JDK 原生(本指南三档哲学第一档)。但分组/转Map 若需工具,用 CollStreamUtil,不要用不存在的 CollUtil.groupBy。 |
| 「BeanUtils.copyProperties 很方便,框架自带」 | Spring/Apache 版源/目标顺序相反,记错会静默拷空;且不报错。MapStruct 编译期类型安全;或 Hutool BeanUtil(顺序固定 source,target)。 |
| 「Executors.newFixedThreadPool 写起来短」 | 其内部是无界队列,任务堆积 OOM。必须 new ThreadPoolExecutor + 有界队列 + 拒绝策略。 |
| 「JDK 8 项目偶尔用个 var 没事」 | 编译失败。JDK 8 不支持 var(10+)。版本门控无例外。 |
C-CHECK 安装决策(需要 Hutool 但项目未引入时触发)
何时触发:判定要用 StrUtil/CollUtil/SecureUtil 等,但读 pom.xml/build.gradle 未发现对应 cn.hutool:hutool-* 模块。
判断是否已引入:读构建文件;无法判断则按下方「模板 D」一并问用户。不要即兴发挥问法——照念下列标准话术,保证一致、一次问全、给出坐标与兜底。
标准话术(按场景照念,勿即兴)
模板 A · 只需 core(多数场景)
这里需要
{场景,如:判断字符串空白},团队规范推荐 Hutool 的{StrUtil.isBlank}(一行、null 安全、避坑)。 检测到pom.xml未引入hutool-core。 A) 引入hutool-core(版本由全局 BOM 统一,见下)→ 我用{StrUtil}。 B) 不引入 → 我手写实现(会保留必要守卫,但不符团队统一规范)。 选 A 还是 B?
模板 B · crypto 场景(任何 MD5/SHA/AES/密码哈希,需 crypto 模块,不是 core)
这里需要
{场景,如:文件 MD5 校验 / 密码哈希},推荐 Hutool 的{SecureUtil.md5 / BCrypt.hashpw}——SecureUtil/DigestUtil/BCrypt/AES全部在hutool-crypto模块(不是 core;只有Base64在 core)。 检测到项目未引入hutool-crypto。 A) 引入hutool-crypto(版本由全局 BOM 统一,见下)→ 我用{SecureUtil.md5 / BCrypt.hashpw}。 B) 不引入 → 我手写MessageDigest(hex 必须%02x补零);密码场景禁无盐/弱盐(彩虹表反查),强烈建议 A。
加密/哈希场景固定走模板 B(crypto 模块):
SecureUtil/DigestUtil都在hutool-crypto(不是 core);密码哈希禁无盐 MD5/SHA,用BCrypt.hashpw,SecureUtil.md5/sha256仅用于非密码场景(校验和、指纹)。
模板 C · 用户拒绝后的兜底说明(必须说 + 代码加注释)
已按你的要求手写。注意 antipattern:
{具体坑,如:MessageDigest 的 hex 必须用 %02x 补零,否则前导零丢失致碰撞}。 已在代码加注释标注这是受控降级,建议后续引入 Hutool 统一。
模板 D · 首次 C-CHECK 合并 JDK 探测(省往返)
两个前置确认:① 目标 JDK(8 / 11-17 / 21+)?② 项目根是否已全局引入
hutoolBOM(团队工具标准,见下)? 确认后我按版本门控 + 工具规范给代码。
分支落地
- 用户同意 → 给对应坐标(见下表;core 最小集,BCrypt 才需 crypto)+ 用库方法。
- 用户拒绝 → 手写实现,必须走对应 reference 的「antipattern 提醒」+ 加注释(模板 C);不可静默降级。
依赖坐标(项目根 pom.xml 全局引入 BOM 一次,仅此一处带版本;模块化,禁 hutool-all):
| 模块 | artifactId | 用途 |
|---|---|---|
| hutool-core(必需最小集) | hutool-core |
StrUtil/CollUtil/CollStreamUtil/DateUtil/ObjectUtil/BeanUtil/Base64/FileUtil/IoUtil/RandomUtil/ExceptionUtil/Assert/HtmlUtil |
| hutool-crypto(按需) | hutool-crypto |
SecureUtil/DigestUtil(MD5/SHA 摘要)、BCrypt(密码哈希)、AES(对称加密) |
Maven(推荐 BOM:版本单一来源,模块不带 version):
<properties>
<hutool.version>5.8.47</hutool.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-bom</artifactId>
<version>${hutool.version}</version>
<type>pom</type>
<scope>import</scope> <!-- 注意:import,不是常规 scope -->
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-core</artifactId>
<!-- 不写 version,由 BOM 统一;需要 MD5/SHA/BCrypt/AES 再加 hutool-crypto -->
</dependency>
</dependencies>
Gradle(platform 方式):
implementation platform('cn.hutool:hutool-bom:5.8.47')
implementation 'cn.hutool:hutool-core'
// implementation 'cn.hutool:hutool-crypto' // MD5/SHA/BCrypt/AES 才需要
Hutool 5.8.x 全线适用(最新稳定版 5.8.47;本地 API 对照官方 javadoc 核实,5.8.x API 稳定)。JDK 8/11/17/21 均兼容。
必选依赖(非 Hutool 工具栈)
项目根
pom.xml引入一次(Hutool 见上)。不引 Guava / commons-lang3(Hutool + Lombok 已覆盖常用能力)。版本可由父 BOM 统一;下列为推荐稳定版。
| 库 | 坐标 | 推荐版本 | 用途 | 详见 |
|---|---|---|---|---|
| Lombok | org.projectlombok:lombok |
1.18.34(provided) |
@Data/@ToString/@Builder/@Slf4j |
06/08 |
| OkHttp3 | com.squareup.okhttp3:okhttp |
4.12.0 |
HTTP 调用 | 04 |
| Jackson | com.fasterxml.jackson.core:jackson-databind |
2.17.1 |
JSON(ObjectMapper 复用单例) |
04 |
| MapStruct | org.mapstruct:mapstruct |
1.5.5.Final |
Bean 拷贝(编译期;无 annotation processor 时按 06 降级 BeanUtil) |
06 |
| SLF4J | org.slf4j:slf4j-api |
2.0.13(JDK 8 用 1.7.36) |
日志门面;实现默认 Logback(JDK 11+ 1.5.38 / JDK 8 1.2.13,门控见 08) |
08 |
annotation processor:Lombok 与 MapStruct 均需配置——Maven 在
maven-compiler-plugin的<annotationProcessorPaths>加两者;Gradle 用annotationProcessor(Lombok 另需compileOnly,MapStruct 另需implementation)。以上版本 JDK 8~21 兼容。
红旗清单(出现以下任一 → 立即停手改写)
- ✗
== null || str.trim().isEmpty()→ 改StrUtil.isBlank - ✗
new SimpleDateFormat(...)作字段/静态 → 改DateUtil/java.time - ✗
a.equals(b)且 a 可能 null → 改ObjectUtil.equal/ 常量在前 - ✗
MessageDigest.getInstance(...)手搓 → 改SecureUtil.md5/sha256 - ✗
log.error("..." + x)或log.error("...", e + msg)→ 改占位符{} - ✗
new Thread(...).start()→ 改线程池 - ✗
Executors.newCachedThreadPool()/newFixedThreadPool()→ 改new ThreadPoolExecutor+ 有界队列 - ✗
BeanUtils.copyProperties(...)未确认版本/顺序 → 改 MapStruct /BeanUtil - ✗
list.subList(from, to)分页 → 改CollUtil.partition/page - ✗ 手拼 JSON 字符串 → 改 Jackson
- ✗ JDK 8 项目出现
var/record/switch 表达式/文本块 → 按版本门控删除 - ✗
Optional.get()前无isPresent/orXxx→ 改orElse/orElseThrow - ✗
e.getMessage()不判空直接用 → 改ExceptionUtil.getMessage(e)(null 安全) - ✗
double/float算钱、new BigDecimal(double)、bd.equals(...)、裸divide→ 改BigDecimal规范(见10)
自查清单(输出代码前逐项过)
- 判空是否用了
StrUtil.isBlank/CollUtil.isEmpty? - 日期是否避免了
SimpleDateFormat共享 /Calendar手算? - 相等判断是否防 NPE?
- 加密是否用了
SecureUtil(没手搓MessageDigest)?密码是否用BCrypt? - Bean 拷贝是否用了 MapStruct/
BeanUtil(源/目标顺序正确)? - 集合分块用
CollUtil.partition;分组/转Map 默认 JDKCollectors(链式组合才用CollStreamUtil)? - IO 是否 try-with-resources?文本是否显式 UTF-8?
- HTTP 是否 OkHttp3?JSON 是否 Jackson(复用单例)?
- 线程池是否
new ThreadPoolExecutor+ 有界队列 + 拒绝策略? - 日志是否用
{}占位符(无字符串拼接)? - 现代 Java 语法是否符合目标 JDK 门控?
- 需要的 Hutool 模块是否已引入(否则触发 C-CHECK)?
- 金额/精确小数是否用了
BigDecimal(禁double算钱)?除法是否给了 scale+RoundingMode?比较是否用compareTo?
使用流程
- 确认适用 + 探测目标 JDK(读构建文件或问用户)。
- 定位 reference:查「域→唯一默认」路由表,读对应文件。
- 编码遵循强约束 + 参考 reference 的 ✗→✓→为什么。
- 遇未引入 Hutool → 触发 C-CHECK。
- 输出前过自查清单 + 红旗清单。
版本与范围
- Hutool 5.8.x(最新稳定 5.8.47,API 对照官方 javadoc 核实);Lombok 1.18.34;OkHttp3 4.12.0;Jackson 2.17.1;MapStruct 1.5.5.Final;SLF4J 2.0.13(具体坐标见「必选依赖」段)。
- JDK 8 为下限,特性按
references/09-modern-java.md门控。 - 团队约定(新项目):禁
hutool-all(模块化引入);不引 commons-lang3(Hutool 覆盖);不引 Guava(命名线程工厂等手写,见05);不引 Spring 专属工具。 - 遗留项目去重原则(构建文件已存在某库时):
- 已引 commons-lang3:用其
StringUtils/ObjectUtils/Validate等,不要再为同类能力引 Hutool(避免两套字符串/集合工具并存)。仅当 commons-lang3 缺该能力(如BCrypt、CollStreamUtil.groupByKey、DateUtil)才引对应 Hutool 模块,并在代码注释标注混用原因。 - 已引 Hutool:按本指南用 Hutool,不再加 commons-lang3。
- 判定法:读
pom.xml/build.gradle先看已有什么 → 跟随既有库 → 仅补缺口。一句话:一个项目一套字符串/集合工具,不混用。
- 已引 commons-lang3:用其
