常用工具类汇总:SM2/SM4/拼音/文件/HTTP 全覆盖
📅 2026-05-15 | 🏷️ Java, 工具类, 国密, Hutool | 📖 阅读约 18 分钟
一、背景
多年项目中积累了不少高频使用的工具代码。它们不算复杂,但每次要用的时候都得翻老项目找,效率很低。这篇文章把它们整理出来,统一收录,方便查阅和复用。
每个工具都包含:使用场景 → 完整代码 → 调用示例。
在动手写工具类之前,先看看成熟库是否已经提供了:
- Hutool 5.8.44(2026-03-11):国人开发的 Java 工具库,覆盖字符串、日期、加密、HTTP、JSON 等 90% 的工具需求,API 简洁
- Apache Commons:
commons-lang3、commons-io、commons-collections等,稳定可靠 - Guava:Google 的 Java 工具库,集合、缓存、并发工具强大
本文在每个工具节的末尾都会给出"成熟库替代方案",优先使用成熟库,只在特定需求时手写。
二、字符串工具
2.1 中文姓名转拼音生成邮箱账号
场景: 新员工入职系统自动生成企业邮箱,规则是"名.姓@company.com",需要支持复姓(如诸葛孔明 → kongming.zhuge@company.com)。
import cn.hutool.extra.pinyin.PinyinUtil;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
/**
* 中文姓名转拼音邮箱账号工具
*
* 支持:
* - 单姓:张三 → san.zhang
* - 复姓:诸葛孔明 → kongming.zhuge
* - 单名:张伟 → wei.zhang
*/
public class PinyinEmailUtil {
/**
* 中国常见复姓集合(可根据需要扩充)
*/
private static final Set<String> COMPOUND_SURNAMES = new HashSet<>(Arrays.asList(
"欧阳", "太史", "端木", "上官", "司马", "东方", "独孤", "南宫",
"万俟", "闻人", "夏侯", "诸葛", "尉迟", "公羊", "赫连", "澹台",
"皇甫", "宗政", "濮阳", "公冶", "太叔", "申屠", "公孙", "慕容",
"仲孙", "钟离", "长孙", "宇文", "司徒", "鲜于", "司空", "闾丘",
"子车", "亓官", "司寇", "巫马", "公西", "颛孙", "壤驷", "公良",
"漆雕", "乐正", "宰父", "谷梁", "拓跋", "夹谷", "轩辕", "令狐",
"段干", "百里", "呼延", "东郭", "南门", "羊舌", "微生", "公户",
"公玉", "公仪", "梁丘", "公仲", "公上", "公门", "公山", "公坚",
"左丘", "公伯", "西门", "公祖", "第五", "公乘", "贯丘", "公皙",
"南荣", "东里", "东宫", "仲长", "子书", "子桑", "即墨", "达奚",
"褚师"
));
/**
* 将中文姓名转换为拼音邮箱账号
*
* @param chineseName 中文姓名(如 "诸葛孔明")
* @param domain 邮箱域名(如 "company.com")
* @return 邮箱账号(如 "kongming.zhuge@company.com")
*/
public static String toEmail(String chineseName, String domain) {
if (chineseName == null || chineseName.length() < 2) {
throw new IllegalArgumentException("姓名至少需要2个字符");
}
String surname;
String givenName;
// 判断是否为复姓(取前两个字匹配)
String firstTwo = chineseName.substring(0, 2);
if (COMPOUND_SURNAMES.contains(firstTwo)) {
surname = firstTwo;
givenName = chineseName.substring(2);
} else {
surname = chineseName.substring(0, 1);
givenName = chineseName.substring(1);
}
// 转拼音(去掉声调,转小写)
String surnamePinyin = PinyinUtil.getPinyin(surname, "");
String givenNamePinyin = PinyinUtil.getPinyin(givenName, "");
return givenNamePinyin + "." + surnamePinyin + "@" + domain;
}
/**
* 仅生成邮箱前缀(不含域名)
*/
public static String toEmailPrefix(String chineseName) {
String prefix = toEmail(chineseName, "").replace("@", "");
// 去除末尾可能多余的点号
if (prefix.endsWith(".")) {
prefix = prefix.substring(0, prefix.length() - 1);
}
return prefix;
}
}使用示例:
public class PinyinEmailDemo {
public static void main(String[] args) {
// 单姓
System.out.println(PinyinEmailUtil.toEmail("张三", "company.com"));
// 输出: san.zhang@company.com
// 复姓
System.out.println(PinyinEmailUtil.toEmail("诸葛孔明", "company.com"));
// 输出: kongming.zhuge@company.com
// 单名
System.out.println(PinyinEmailUtil.toEmail("张伟", "company.com"));
// 输出: wei.zhang@company.com
// 仅前缀
System.out.println(PinyinEmailUtil.toEmailPrefix("李小龙"));
// 输出: xiaolong.li
}
}依赖:
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-extra</artifactId>
<version>5.8.44</version>
</dependency>
<!-- pinyin4j 底层引擎 -->
<dependency>
<groupId>com.belerweb</groupId>
<artifactId>pinyin4j</artifactId>
<version>2.5.1</version>
</dependency>拼音库对多音字的处理是基于词频统计的,不保证 100% 正确。如"单"可以是 dān 或 shàn(单姓),拼音库通常输出 dān。对于人名场景,建议:
- 维护一个多音字姓氏映射表
- 在系统中允许用户手动修改生成的邮箱前缀
三、加密工具
3.1 SM2 密钥处理(BigInteger 前导 0 修复)
场景: 国密 SM2 密钥通常是十六进制字符串,转 BigInteger 时会丢失前导 0,导致密钥长度不足 64 字节(32 字节 × 2 hex),签名或解密失败。
import java.math.BigInteger;
/**
* SM2 密钥处理工具
*
* 解决 BigInteger 丢失前导 0 的经典坑
*/
public class SM2KeyUtil {
/**
* SM2 密钥的固定长度(字节数)
*/
private static final int SM2_KEY_BYTE_LENGTH = 32;
/**
* 将十六进制密钥字符串转换为固定长度的字节数组
*
* 核心问题:new BigInteger("00abc...") 会丢弃前导 0,
* 导致 toByteArray() 返回的数组长度 < 32。
* 而 SM2 算法要求密钥必须是 32 字节。
*
* @param hexKey 十六进制密钥字符串(64 个字符)
* @return 固定 32 字节的密钥字节数组
*/
public static byte[] hexToFixedBytes(String hexKey) {
BigInteger bigInt = new BigInteger(hexKey, 16);
byte[] rawBytes = bigInt.toByteArray();
// 如果长度刚好 32 字节,直接返回
if (rawBytes.length == SM2_KEY_BYTE_LENGTH) {
return rawBytes;
}
// 如果长度为 33 字节(BigInteger 对正数补了符号位 0),去掉第一个字节
if (rawBytes.length == SM2_KEY_BYTE_LENGTH + 1 && rawBytes[0] == 0) {
byte[] fixed = new byte[SM2_KEY_BYTE_LENGTH];
System.arraycopy(rawBytes, 1, fixed, 0, SM2_KEY_BYTE_LENGTH);
return fixed;
}
// 如果长度 < 32(丢失了前导 0),左补 0
if (rawBytes.length < SM2_KEY_BYTE_LENGTH) {
byte[] fixed = new byte[SM2_KEY_BYTE_LENGTH];
// 从右往左填充,前面补 0
System.arraycopy(rawBytes, 0, fixed, SM2_KEY_BYTE_LENGTH - rawBytes.length, rawBytes.length);
return fixed;
}
throw new IllegalArgumentException("无效的 SM2 密钥长度: " + rawBytes.length);
}
/**
* 从 PEM 格式私钥中提取十六进制密钥
*
* @param pemContent PEM 内容
* @return 十六进制密钥字符串
*/
public static String extractHexFromPem(String pemContent) {
// 去掉 PEM 头尾和换行
String base64 = pemContent
.replace("-----BEGIN EC PRIVATE KEY-----", "")
.replace("-----END EC PRIVATE KEY-----", "")
.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replaceAll("\\s+", "");
byte[] der = java.util.Base64.getDecoder().decode(base64);
// DER 编码的 EC 私钥中,最后 32 字节是私钥值
byte[] privateKey = new byte[SM2_KEY_BYTE_LENGTH];
System.arraycopy(der, der.length - SM2_KEY_BYTE_LENGTH, privateKey, 0, SM2_KEY_BYTE_LENGTH);
return bytesToHex(privateKey);
}
/**
* 字节数组转十六进制字符串
*/
public static String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02x", b & 0xff));
}
return sb.toString();
}
}Base64 将每 3 个字节编码为 4 个字符。对于 n 字节的输入,Base64 长度 = 4 × ⌈n/3⌉。
例如,SM2 密钥 32 字节 → Base64 长度 = 4 × 11 = 44 个字符。
PEM 格式会在每 64 个字符后插入换行符,所以实际文件会更长。
3.2 SM4 加解密封装
场景: 业务数据加密存储,SM4 是国密对称加密算法(类似 AES),128 位密钥,16 字节分组。
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
/**
* SM4 对称加密工具类
*
* 支持 ECB 和 CBC 模式,默认 PKCS5Padding。
* 注意:标准 JDK 不内置 SM4,需要先引入并注册 Bouncy Castle 等 JCE Provider。
*/
public class SM4Util {
private static final String ALGORITHM = "SM4";
private static final String TRANSFORMATION_ECB = "SM4/ECB/PKCS5Padding";
private static final String TRANSFORMATION_CBC = "SM4/CBC/PKCS5Padding";
/**
* SM4-ECB 加密
*
* @param plaintext 明文
* @param hexKey 十六进制密钥(32 个字符 = 16 字节)
* @return Base64 编码的密文
*/
public static String encryptEcb(String plaintext, String hexKey) {
try {
byte[] keyBytes = hexStringToBytes(hexKey);
SecretKeySpec keySpec = new SecretKeySpec(keyBytes, ALGORITHM);
Cipher cipher = Cipher.getInstance(TRANSFORMATION_ECB);
cipher.init(Cipher.ENCRYPT_MODE, keySpec);
byte[] encrypted = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(encrypted);
} catch (Exception e) {
throw new RuntimeException("SM4 ECB 加密失败", e);
}
}
/**
* SM4-ECB 解密
*
* @param ciphertext Base64 编码的密文
* @param hexKey 十六进制密钥
* @return 明文
*/
public static String decryptEcb(String ciphertext, String hexKey) {
try {
byte[] keyBytes = hexStringToBytes(hexKey);
SecretKeySpec keySpec = new SecretKeySpec(keyBytes, ALGORITHM);
Cipher cipher = Cipher.getInstance(TRANSFORMATION_ECB);
cipher.init(Cipher.DECRYPT_MODE, keySpec);
byte[] decrypted = cipher.doFinal(Base64.getDecoder().decode(ciphertext));
return new String(decrypted, StandardCharsets.UTF_8);
} catch (Exception e) {
throw new RuntimeException("SM4 ECB 解密失败", e);
}
}
/**
* SM4-CBC 加密
*
* @param plaintext 明文
* @param hexKey 十六进制密钥(32 字符)
* @param hexIv 十六进制 IV 向量(32 字符)
* @return Base64 编码的密文
*/
public static String encryptCbc(String plaintext, String hexKey, String hexIv) {
try {
byte[] keyBytes = hexStringToBytes(hexKey);
byte[] ivBytes = hexStringToBytes(hexIv);
SecretKeySpec keySpec = new SecretKeySpec(keyBytes, ALGORITHM);
IvParameterSpec ivSpec = new IvParameterSpec(ivBytes);
Cipher cipher = Cipher.getInstance(TRANSFORMATION_CBC);
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
byte[] encrypted = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(encrypted);
} catch (Exception e) {
throw new RuntimeException("SM4 CBC 加密失败", e);
}
}
/**
* SM4-CBC 解密
*/
public static String decryptCbc(String ciphertext, String hexKey, String hexIv) {
try {
byte[] keyBytes = hexStringToBytes(hexKey);
byte[] ivBytes = hexStringToBytes(hexIv);
SecretKeySpec keySpec = new SecretKeySpec(keyBytes, ALGORITHM);
IvParameterSpec ivSpec = new IvParameterSpec(ivBytes);
Cipher cipher = Cipher.getInstance(TRANSFORMATION_CBC);
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
byte[] decrypted = cipher.doFinal(Base64.getDecoder().decode(ciphertext));
return new String(decrypted, StandardCharsets.UTF_8);
} catch (Exception e) {
throw new RuntimeException("SM4 CBC 解密失败", e);
}
}
private static byte[] hexStringToBytes(String hex) {
if (hex == null || (hex.length() & 1) == 1) {
throw new IllegalArgumentException("十六进制字符串长度必须为偶数");
}
int len = hex.length();
byte[] bytes = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
int high = Character.digit(hex.charAt(i), 16);
int low = Character.digit(hex.charAt(i + 1), 16);
if (high < 0 || low < 0) {
throw new IllegalArgumentException("包含非法十六进制字符: " + hex);
}
bytes[i / 2] = (byte) ((high << 4) + low);
}
return bytes;
}
}使用示例:
public class SM4Demo {
public static void main(String[] args) {
String key = "0123456789abcdef0123456789abcdef"; // 16 字节 = 32 hex
String plaintext = "这是一段需要加密的敏感数据";
// ECB 模式
String encrypted = SM4Util.encryptEcb(plaintext, key);
System.out.println("密文: " + encrypted);
String decrypted = SM4Util.decryptEcb(encrypted, key);
System.out.println("明文: " + decrypted);
// CBC 模式(更安全,推荐)
String iv = "abcdef0123456789abcdef0123456789";
String encryptedCbc = SM4Util.encryptCbc(plaintext, key, iv);
System.out.println("CBC密文: " + encryptedCbc);
String decryptedCbc = SM4Util.decryptCbc(encryptedCbc, key, iv);
System.out.println("CBC明文: " + decryptedCbc);
}
}3.3 国密工具类推荐
| 库 | 特点 | 依赖 | 推荐度 |
|---|---|---|---|
| hutool-crypto | hutool 生态,API 简洁,SM2/SM3/SM4 全支持 | cn.hutool:hutool-crypto | ⭐⭐⭐⭐⭐ |
| Bouncy Castle | 底层库,功能覆盖较全,国密 SSL 支持 | org.bouncycastle:bcprov-jdk18on | ⭐⭐⭐⭐ |
| gmhelper | 专注国密,性能好 | 基于 Bouncy Castle | ⭐⭐⭐ |
| gmspring-boot-starter | Spring Boot 集成,开箱即用 | 参见 GitHub | ⭐⭐⭐⭐ |
如果你的项目已经引入了 Hutool,直接使用 hutool-crypto 即可,代码量减少 80%:
import cn.hutool.core.util.HexUtil;
import cn.hutool.crypto.symmetric.SM4;
// SM4 加解密(Hutool 风格)
SM4 sm4 = new SM4(HexUtil.decodeHex(hexKey));
String encrypted = sm4.encryptBase64(plaintext);
String decrypted = sm4.decryptStr(encrypted);
// SM2 签名验签(Hutool 风格)
import cn.hutool.crypto.asymmetric.SM2;
SM2 sm2 = new SM2(privateKeyHex, publicKeyHex);
byte[] sign = sm2.sign(content.getBytes());
boolean verified = sm2.verify(content.getBytes(), sign);依赖:
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-crypto</artifactId>
<version>5.8.44</version>
</dependency>四、文件工具
4.1 文件读写
场景: 读取配置文件、导出数据到文件、日志文件处理。
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.List;
import java.util.stream.Collectors;
/**
* 文件读写工具类
*/
public class FileUtil {
/**
* 读取文本文件内容为字符串
*
* @param filePath 文件路径
* @return 文件内容
*/
public static String readFile(String filePath) {
try {
byte[] bytes = Files.readAllBytes(Paths.get(filePath));
return new String(bytes, StandardCharsets.UTF_8);
} catch (IOException e) {
throw new RuntimeException("读取文件失败: " + filePath, e);
}
}
/**
* 读取文件为行列表
*
* @param filePath 文件路径
* @return 行列表
*/
public static List<String> readLines(String filePath) {
try {
return Files.readAllLines(Paths.get(filePath), StandardCharsets.UTF_8);
} catch (IOException e) {
throw new RuntimeException("读取文件失败: " + filePath, e);
}
}
/**
* 写入字符串到文件(覆盖模式)
*
* @param filePath 文件路径
* @param content 内容
*/
public static void writeFile(String filePath, String content) {
try {
// 自动创建父目录
Path path = Paths.get(filePath);
if (path.getParent() != null) {
Files.createDirectories(path.getParent());
}
Files.write(path, content.getBytes(StandardCharsets.UTF_8));
} catch (IOException e) {
throw new RuntimeException("写入文件失败: " + filePath, e);
}
}
/**
* 追加内容到文件
*
* @param filePath 文件路径
* @param content 内容
*/
public static void appendFile(String filePath, String content) {
try {
Path path = Paths.get(filePath);
if (path.getParent() != null) {
Files.createDirectories(path.getParent());
}
Files.write(path, content.getBytes(StandardCharsets.UTF_8),
StandardOpenOption.CREATE, StandardOpenOption.APPEND);
} catch (IOException e) {
throw new RuntimeException("追加文件失败: " + filePath, e);
}
}
/**
* 读取 classpath 资源文件
*
* @param resourcePath 资源路径(相对于 classpath)
* @return 文件内容
*/
public static String readClasspathResource(String resourcePath) {
try (InputStream is = FileUtil.class.getClassLoader().getResourceAsStream(resourcePath)) {
if (is == null) {
throw new FileNotFoundException("资源文件不存在: " + resourcePath);
}
BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8));
return reader.lines().collect(Collectors.joining("\n"));
} catch (IOException e) {
throw new RuntimeException("读取资源文件失败: " + resourcePath, e);
}
}
}使用示例:
// 读取文件
String content = FileUtil.readFile("/data/config/app.properties");
// 读取 classpath 资源
String template = FileUtil.readClasspathResource("templates/email.html");
// 写入文件
FileUtil.writeFile("/data/output/result.txt", "处理完成");
// 追加日志
FileUtil.appendFile("/data/logs/audit.log", "2024-01-01 操作记录...");Hutool 的 cn.hutool.core.io.FileUtil 提供了更丰富的文件操作:
import cn.hutool.core.io.FileUtil;
// 读取文件
String content = FileUtil.readUtf8String("/data/config/app.properties");
List<String> lines = FileUtil.readUtf8Lines("/data/config/app.properties");
// 写入文件
FileUtil.writeUtf8String("内容", "/data/output/result.txt");
// 读取 classpath 资源
String template = FileUtil.readUtf8String(
FileUtil.file("templates/email.html") // 自动从 classpath 加载
);
// 文件复制、移动、删除
FileUtil.copy(srcPath, destPath, true); // 覆盖模式
FileUtil.move(srcPath, destPath, true);
FileUtil.del("/data/temp");4.2 PDF 生成工具
详细实现请参考本系列上一篇文章:Java PDF 模板生成实战
/**
* PDF 生成快捷方法(基于 iText 5.x)
*
* @param templatePath PDF 模板路径
* @param outputPath 输出路径
* @param data 表单数据 Map
*/
public static void generatePdf(String templatePath, String outputPath, Map<String, String> data) {
PdfUtil.fillPdf(templatePath, outputPath, data);
}五、集合工具
5.1 Bean 转 Map
场景: 将 Java Bean 属性转为 Map<String, String>,常用于日志记录、数据导出、表单填充。
import java.beans.BeanInfo;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
/**
* Bean 转 Map 工具
*/
public class BeanUtil {
/**
* 将 Java Bean 转为 Map<String, String>
*
* @param bean Java Bean 对象
* @return 属性名 → 属性值的 Map(值调用 toString())
*/
public static Map<String, String> beanToStringMap(Object bean) {
if (bean == null) {
return new HashMap<>();
}
Map<String, String> map = new HashMap<>();
try {
BeanInfo beanInfo = Introspector.getBeanInfo(bean.getClass(), Object.class);
PropertyDescriptor[] pds = beanInfo.getPropertyDescriptors();
for (PropertyDescriptor pd : pds) {
Method getter = pd.getReadMethod();
if (getter != null) {
Object value = getter.invoke(bean);
map.put(pd.getName(), value != null ? value.toString() : "");
}
}
} catch (Exception e) {
throw new RuntimeException("Bean 转 Map 失败", e);
}
return map;
}
/**
* 将 Java Bean 转为 Map<String, Object>(保留原始类型)
*/
public static Map<String, Object> beanToMap(Object bean) {
if (bean == null) {
return new HashMap<>();
}
Map<String, Object> map = new HashMap<>();
try {
BeanInfo beanInfo = Introspector.getBeanInfo(bean.getClass(), Object.class);
PropertyDescriptor[] pds = beanInfo.getPropertyDescriptors();
for (PropertyDescriptor pd : pds) {
Method getter = pd.getReadMethod();
if (getter != null) {
map.put(pd.getName(), getter.invoke(bean));
}
}
} catch (Exception e) {
throw new RuntimeException("Bean 转 Map 失败", e);
}
return map;
}
/**
* Map 转 Bean
*
* @param map 数据 Map
* @param clazz 目标类型
* @return Bean 对象
*/
public static <T> T mapToBean(Map<String, Object> map, Class<T> clazz) {
try {
T bean = clazz.getDeclaredConstructor().newInstance();
BeanInfo beanInfo = Introspector.getBeanInfo(clazz, Object.class);
PropertyDescriptor[] pds = beanInfo.getPropertyDescriptors();
for (PropertyDescriptor pd : pds) {
Object value = map.get(pd.getName());
if (value != null && pd.getWriteMethod() != null) {
// 简单类型转换
Class<?> propType = pd.getPropertyType();
if (propType == String.class) {
pd.getWriteMethod().invoke(bean, value.toString());
} else if (propType == Integer.class || propType == int.class) {
pd.getWriteMethod().invoke(bean, Integer.parseInt(value.toString()));
} else if (propType == Long.class || propType == long.class) {
pd.getWriteMethod().invoke(bean, Long.parseLong(value.toString()));
} else {
pd.getWriteMethod().invoke(bean, value);
}
}
}
return bean;
} catch (Exception e) {
throw new RuntimeException("Map 转 Bean 失败", e);
}
}
}import cn.hutool.core.bean.BeanUtil;
// Bean → Map
Map<String, Object> map = BeanUtil.beanToMap(user);
// Map → Bean
UserInfo user = BeanUtil.mapToBean(map, UserInfo.class, true);
// Bean 拷贝(同名属性自动映射)
UserInfo target = new UserInfo();
BeanUtil.copyProperties(source, target);
// 嵌套 Bean 拷贝(忽略空值)
BeanUtil.copyProperties(source, target, CopyOptions.create().setIgnoreNullValue(true));5.2 集合判空
场景: 遍历集合前的安全检查,避免 NPE。
import java.util.Collection;
import java.util.Map;
/**
* 集合判空工具
*/
public class CollUtil {
/**
* Collection 是否为空(null 或 size==0)
*/
public static boolean isEmpty(Collection<?> collection) {
return collection == null || collection.isEmpty();
}
/**
* Collection 是否非空
*/
public static boolean isNotEmpty(Collection<?> collection) {
return !isEmpty(collection);
}
/**
* Map 是否为空
*/
public static boolean isEmpty(Map<?, ?> map) {
return map == null || map.isEmpty();
}
/**
* Map 是否非空
*/
public static boolean isNotEmpty(Map<?, ?> map) {
return !isEmpty(map);
}
/**
* 数组是否为空
*/
public static boolean isEmpty(Object[] array) {
return array == null || array.length == 0;
}
}使用示例:
List<String> list = queryUsers();
// ❌ 不安全
if (list != null && list.size() > 0) { ... }
// ✅ 推荐
if (CollUtil.isNotEmpty(list)) {
list.forEach(System.out::println);
}
// 也可以用 hutool(如果项目已引入)
// cn.hutool.core.collection.CollUtil.isEmpty(list)六、日期工具
6.1 常用日期格式化
场景: 日志记录、文件命名、接口返回、数据库交互中的日期格式转换。
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.util.Date;
/**
* 日期格式化工具
*
* 基于 Java 8+ 的 java.time API(推荐,线程安全)
*/
public class DateUtil {
// ========== 常用格式 ==========
public static final String DATE_PATTERN = "yyyy-MM-dd";
public static final String DATETIME_PATTERN = "yyyy-MM-dd HH:mm:ss";
public static final String COMPACT_DATE = "yyyyMMdd";
public static final String COMPACT_DATETIME = "yyyyMMddHHmmss";
public static final String CHINESE_DATE = "yyyy年MM月dd日";
// ========== 预定义 Formatter(线程安全,可复用) ==========
public static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern(DATE_PATTERN);
public static final DateTimeFormatter DATETIME_FMT = DateTimeFormatter.ofPattern(DATETIME_PATTERN);
public static final DateTimeFormatter COMPACT_DATE_FMT = DateTimeFormatter.ofPattern(COMPACT_DATE);
public static final DateTimeFormatter COMPACT_DATETIME_FMT = DateTimeFormatter.ofPattern(COMPACT_DATETIME);
public static final DateTimeFormatter CHINESE_DATE_FMT = DateTimeFormatter.ofPattern(CHINESE_DATE);
/**
* 当前日期时间字符串
*/
public static String now() {
return LocalDateTime.now().format(DATETIME_FMT);
}
/**
* 当前日期字符串
*/
public static String today() {
return LocalDate.now().format(DATE_FMT);
}
/**
* 指定格式的当前时间
*/
public static String now(String pattern) {
return LocalDateTime.now().format(DateTimeFormatter.ofPattern(pattern));
}
/**
* 格式化 LocalDateTime
*/
public static String format(LocalDateTime dateTime, String pattern) {
if (dateTime == null) return "";
return dateTime.format(DateTimeFormatter.ofPattern(pattern));
}
/**
* 格式化 LocalDate
*/
public static String format(LocalDate date, String pattern) {
if (date == null) return "";
return date.format(DateTimeFormatter.ofPattern(pattern));
}
/**
* 解析字符串为 LocalDateTime
*/
public static LocalDateTime parseDateTime(String dateTimeStr, String pattern) {
return LocalDateTime.parse(dateTimeStr, DateTimeFormatter.ofPattern(pattern));
}
/**
* Date 转 LocalDateTime
*/
public static LocalDateTime toLocalDateTime(Date date) {
if (date == null) return null;
return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
}
/**
* LocalDateTime 转 Date
*/
public static Date toDate(LocalDateTime localDateTime) {
if (localDateTime == null) return null;
return Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant());
}
}6.2 时间区间计算
场景: 计算两个时间点之间的天数/小时数、判断是否在某个时间范围内。
import java.time.*;
import java.time.temporal.ChronoUnit;
/**
* 时间区间计算工具
*/
public class DateRangeUtil {
/**
* 计算两个日期之间的天数
*
* @param start 开始日期
* @param end 结束日期
* @return 天数(可为负数)
*/
public static long daysBetween(LocalDate start, LocalDate end) {
return ChronoUnit.DAYS.between(start, end);
}
/**
* 计算两个时间点之间的小时数
*/
public static long hoursBetween(LocalDateTime start, LocalDateTime end) {
return ChronoUnit.HOURS.between(start, end);
}
/**
* 计算两个时间点之间的分钟数
*/
public static long minutesBetween(LocalDateTime start, LocalDateTime end) {
return ChronoUnit.MINUTES.between(start, end);
}
/**
* 判断当前时间是否在指定范围内
*
* @param start 开始时间
* @param end 结束时间
* @return 是否在范围内
*/
public static boolean isNowBetween(LocalDateTime start, LocalDateTime end) {
LocalDateTime now = LocalDateTime.now();
return !now.isBefore(start) && !now.isAfter(end);
}
/**
* 获取今天的开始时间(00:00:00)
*/
public static LocalDateTime startOfToday() {
return LocalDate.now().atStartOfDay();
}
/**
* 获取今天的结束时间(23:59:59)
*/
public static LocalDateTime endOfToday() {
return LocalDate.now().atTime(LocalTime.MAX);
}
/**
* 获取本周的开始时间(周一 00:00:00)
*/
public static LocalDateTime startOfWeek() {
return LocalDate.now().with(DayOfWeek.MONDAY).atStartOfDay();
}
/**
* 获取本月的开始时间
*/
public static LocalDateTime startOfMonth() {
return LocalDate.now().withDayOfMonth(1).atStartOfDay();
}
}使用示例:
// 计算两个日期之间的天数
LocalDate start = LocalDate.of(2024, 1, 1);
LocalDate end = LocalDate.of(2024, 12, 31);
long days = DateRangeUtil.daysBetween(start, end);
System.out.println("相差 " + days + " 天"); // 相差 365 天
// 判断是否在活动期间
LocalDateTime activityStart = LocalDateTime.of(2024, 11, 11, 0, 0);
LocalDateTime activityEnd = LocalDateTime.of(2024, 11, 11, 23, 59);
if (DateRangeUtil.isNowBetween(activityStart, activityEnd)) {
System.out.println("双十一大促进行中!");
}
// 格式化
System.out.println(DateUtil.now()); // 2024-01-15 14:30:00
System.out.println(DateUtil.today()); // 2024-01-15
System.out.println(DateUtil.now("yyyyMMdd")); // 20240115import cn.hutool.core.date.DateUtil;
// 当前时间
String now = DateUtil.now(); // 2026-05-15 20:30:00
String today = DateUtil.today(); // 2026-05-15
// 解析日期
Date date = DateUtil.parse("2026-05-15");
Date date2 = DateUtil.parse("20260515", "yyyyMMdd");
// 格式化
String str = DateUtil.formatDate(date);
String str2 = DateUtil.format(date, "yyyy年MM月dd日");
// 时间差
long days = DateUtil.between(date1, date2, DateUnit.DAY);
// 昨天、明天、本周、本月
Date beginOfWeek = DateUtil.beginOfWeek(new Date());
Date endOfMonth = DateUtil.endOfMonth(new Date());七、HTTP 工具
7.1 RestTemplate 封装
场景: Spring 项目中调用第三方接口,统一处理超时、异常、日志。
import org.springframework.http.*;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import java.util.Map;
/**
* RestTemplate 封装工具
*
* 统一配置超时、Header、异常处理
*/
public class HttpUtil {
private static final RestTemplate REST_TEMPLATE;
static {
// 配置连接超时和读取超时
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(5000); // 连接超时 5s
factory.setReadTimeout(30000); // 读取超时 30s
REST_TEMPLATE = new RestTemplate(factory);
}
/**
* GET 请求
*
* @param url 请求 URL(可包含 {key} 占位符)
* @param clazz 响应类型
* @param uriVars URL 占位符参数
* @return 响应体
*/
public static <T> T get(String url, Class<T> clazz, Object... uriVars) {
ResponseEntity<T> response = REST_TEMPLATE.getForEntity(url, clazz, uriVars);
return response.getBody();
}
/**
* GET 请求(带 Header)
*/
public static <T> T getWithHeaders(String url, Class<T> clazz,
Map<String, String> headers, Object... uriVars) {
HttpHeaders httpHeaders = new HttpHeaders();
headers.forEach(httpHeaders::set);
HttpEntity<Void> entity = new HttpEntity<>(httpHeaders);
ResponseEntity<T> response = REST_TEMPLATE.exchange(
url, HttpMethod.GET, entity, clazz, uriVars
);
return response.getBody();
}
/**
* POST JSON 请求
*
* @param url 请求 URL
* @param body 请求体(会序列化为 JSON)
* @param clazz 响应类型
* @return 响应体
*/
public static <T> T postJson(String url, Object body, Class<T> clazz) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<Object> entity = new HttpEntity<>(body, headers);
ResponseEntity<T> response = REST_TEMPLATE.postForEntity(url, entity, clazz);
return response.getBody();
}
/**
* POST JSON 请求(带自定义 Header)
*/
public static <T> T postJson(String url, Object body, Class<T> clazz,
Map<String, String> headers) {
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setContentType(MediaType.APPLICATION_JSON);
headers.forEach(httpHeaders::set);
HttpEntity<Object> entity = new HttpEntity<>(body, httpHeaders);
ResponseEntity<T> response = REST_TEMPLATE.postForEntity(url, entity, clazz);
return response.getBody();
}
/**
* POST 表单请求
*/
public static <T> T postForm(String url, Map<String, String> formData, Class<T> clazz) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
MultiValueMap<String, String> form = new LinkedMultiValueMap<>();
formData.forEach(form::add);
HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<>(form, headers);
ResponseEntity<T> response = REST_TEMPLATE.postForEntity(url, entity, clazz);
return response.getBody();
}
/**
* 返回原始 ResponseEntity(需要读 Header 或状态码时使用)
*/
public static <T> ResponseEntity<T> exchange(String url, HttpMethod method,
Object body, Class<T> clazz,
Map<String, String> headers) {
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setContentType(MediaType.APPLICATION_JSON);
if (headers != null) {
headers.forEach(httpHeaders::set);
}
HttpEntity<Object> entity = new HttpEntity<>(body, httpHeaders);
return REST_TEMPLATE.exchange(url, method, entity, clazz);
}
}使用示例:
// GET 请求
String result = HttpUtil.get("https://api.example.com/users/{id}", String.class, 123);
// POST JSON
UserInfo user = new UserInfo();
user.setName("张三");
Map<String, Object> response = HttpUtil.postJson(
"https://api.example.com/users", user, Map.class
);
// 带 Token 的请求
Map<String, String> headers = Map.of("Authorization", "Bearer xxx-token");
String data = HttpUtil.getWithHeaders(
"https://api.example.com/data", String.class, headers
);Spring Boot 3.2+ 推荐使用 RestClient 替代 RestTemplate,API 更现代:
import org.springframework.web.client.RestClient;
RestClient client = RestClient.builder()
.baseUrl("https://api.example.com")
.defaultHeader("Authorization", "Bearer token")
.build();
String result = client.get()
.uri("/users/{id}", 123)
.retrieve()
.body(String.class);7.2 OkHttp 封装
场景: 非 Spring 项目或需要更灵活的 HTTP 客户端(连接池复用、拦截器、异步请求)。
import okhttp3.*;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* OkHttp 封装工具
*
* 优点:连接池复用、支持 HTTP/2、拦截器机制
*/
public class OkHttpUtil {
private static final OkHttpClient CLIENT;
private static final ObjectMapper MAPPER = new ObjectMapper();
private static final MediaType JSON_MEDIA = MediaType.parse("application/json; charset=utf-8");
static {
CLIENT = new OkHttpClient.Builder()
.connectTimeout(5, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.connectionPool(new ConnectionPool(10, 5, TimeUnit.MINUTES)) // 连接池
.build();
}
/**
* GET 请求
*
* @param url 请求 URL
* @param headers 请求头(可为 null)
* @return 响应字符串
*/
public static String get(String url, Map<String, String> headers) throws IOException {
Request.Builder builder = new Request.Builder().url(url).get();
if (headers != null) {
headers.forEach(builder::addHeader);
}
try (Response response = CLIENT.newCall(builder.build()).execute()) {
if (!response.isSuccessful()) {
throw new IOException("请求失败: " + response.code() + " " + response.message());
}
ResponseBody body = response.body();
return body != null ? body.string() : "";
}
}
/**
* POST JSON 请求
*
* @param url 请求 URL
* @param body 请求体对象(自动序列化为 JSON)
* @param headers 请求头(可为 null)
* @return 响应字符串
*/
public static String postJson(String url, Object body, Map<String, String> headers) throws IOException {
String json = MAPPER.writeValueAsString(body);
RequestBody requestBody = RequestBody.create(json, JSON_MEDIA);
Request.Builder builder = new Request.Builder().url(url).post(requestBody);
builder.addHeader("Content-Type", "application/json");
if (headers != null) {
headers.forEach(builder::addHeader);
}
try (Response response = CLIENT.newCall(builder.build()).execute()) {
if (!response.isSuccessful()) {
throw new IOException("请求失败: " + response.code() + " " + response.message());
}
ResponseBody responseBody = response.body();
return responseBody != null ? responseBody.string() : "";
}
}
/**
* POST 表单请求
*/
public static String postForm(String url, Map<String, String> formData,
Map<String, String> headers) throws IOException {
FormBody.Builder formBuilder = new FormBody.Builder();
formData.forEach(formBuilder::add);
Request.Builder builder = new Request.Builder().url(url).post(formBuilder.build());
if (headers != null) {
headers.forEach(builder::addHeader);
}
try (Response response = CLIENT.newCall(builder.build()).execute()) {
if (!response.isSuccessful()) {
throw new IOException("请求失败: " + response.code() + " " + response.message());
}
ResponseBody body = response.body();
return body != null ? body.string() : "";
}
}
/**
* POST JSON 并反序列化响应
*
* @param url 请求 URL
* @param body 请求体
* @param clazz 响应类型
* @param headers 请求头
* @return 反序列化后的响应对象
*/
public static <T> T postJsonAndParse(String url, Object body,
Class<T> clazz, Map<String, String> headers) throws IOException {
String responseStr = postJson(url, body, headers);
return MAPPER.readValue(responseStr, clazz);
}
}使用示例:
// GET 请求
String result = OkHttpUtil.get("https://api.example.com/data", null);
// POST JSON
Map<String, Object> requestData = Map.of("name", "张三", "age", 28);
Map<String, String> headers = Map.of("Authorization", "Bearer token123");
String response = OkHttpUtil.postJson("https://api.example.com/users", requestData, headers);
// POST 并自动反序列化
UserResponse userResponse = OkHttpUtil.postJsonAndParse(
"https://api.example.com/users", requestData, UserResponse.class, headers
);import cn.hutool.http.HttpUtil;
// GET 请求
String result = HttpUtil.get("https://api.example.com/users/123");
// POST JSON
String json = JSONUtil.toJsonStr(requestData);
String response = HttpUtil.createPost("https://api.example.com/users")
.body(json)
.header("Authorization", "Bearer token")
.execute()
.body();
// 下载文件
HttpUtil.downloadFile("https://example.com/file.zip", "/data/downloads/");
// 带超时和重试
String result = HttpUtil.createGet("https://api.example.com/data")
.timeout(5000)
.execute()
.body();八、成熟实践总结
| 工具类别 | 手写方案 | 成熟库替代 | 推荐 |
|---|---|---|---|
| 拼音转换 | 自定义工具类 | Hutool PinyinUtil | Hutool |
| 国密加密 | 自定义 SM4Util | Hutool SM4 / Bouncy Castle | Hutool |
| 文件操作 | java.nio.file.Files | Hutool FileUtil / Commons IO | Hutool / Commons |
| Bean 转 Map | java.beans.Introspector | Hutool BeanUtil / Commons BeanUtils | Hutool |
| 集合判空 | 自定义 CollUtil | Hutool CollUtil / Commons Collections | Hutool |
| 日期处理 | java.time API | Hutool DateUtil | java.time(首选)/ Hutool |
| HTTP 调用 | RestClient / RestTemplate / OkHttp | Hutool HttpUtil | Spring Boot 3.2+ 优先 RestClient |
| JSON 处理 | Jackson | Hutool JSONUtil | Jackson(Spring 项目) |
只需引入需要的模块,不必全量引入:
<!-- 核心工具(字符串、日期、集合、Bean、文件等) -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-core</artifactId>
<version>5.8.44</version>
</dependency>
<!-- 加密工具(SM2/SM3/SM4、AES、RSA 等) -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-crypto</artifactId>
<version>5.8.44</version>
</dependency>
<!-- HTTP 工具 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-http</artifactId>
<version>5.8.44</version>
</dependency>
<!-- JSON 工具 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-json</artifactId>
<version>5.8.44</version>
</dependency>
<!-- 拼音工具 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-extra</artifactId>
<version>5.8.44</version>
</dependency>代码组织建议:
- 收集到
common或util模块中,统一管理 - 每个工具类加
@UtilityClass(Lombok)或私有构造函数,禁止实例化 - 工具方法都应该是
static且无副作用 - 异常统一包装为
RuntimeException,或定义业务异常 - 写好 Javadoc,尤其是参数说明和使用示例
- 优先使用成熟库,只在成熟库不满足需求时手写
九、参考资料
- Hutool 官方文档
- Hutool GitHub Releases
- Bouncy Castle 官网
- Java 8 Time API 教程
- OkHttp 官方文档
- Apache Commons Lang
- SM 国密算法标准
系列导航: Java 工程化实战系列 | 上一篇:Java PDF 模板生成实战