← 返回

常用工具类汇总:SM2/SM4/拼音/文件/HTTP 全覆盖

📅 2026-05-15 | 🏷️ Java, 工具类, 国密, Hutool | 📖 阅读约 18 分钟

一、背景

多年项目中积累了不少高频使用的工具代码。它们不算复杂,但每次要用的时候都得翻老项目找,效率很低。这篇文章把它们整理出来,统一收录,方便查阅和复用。

每个工具都包含:使用场景 → 完整代码 → 调用示例

不要重复造轮子

在动手写工具类之前,先看看成熟库是否已经提供了:

  • Hutool 5.8.44(2026-03-11):国人开发的 Java 工具库,覆盖字符串、日期、加密、HTTP、JSON 等 90% 的工具需求,API 简洁
  • Apache Commonscommons-lang3commons-iocommons-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ānshàn(单姓),拼音库通常输出 dān。对于人名场景,建议:

  1. 维护一个多音字姓氏映射表
  2. 在系统中允许用户手动修改生成的邮箱前缀

三、加密工具

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 编码长度计算

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-cryptohutool 生态,API 简洁,SM2/SM3/SM4 全支持cn.hutool:hutool-crypto⭐⭐⭐⭐⭐
Bouncy Castle底层库,功能覆盖较全,国密 SSL 支持org.bouncycastle:bcprov-jdk18on⭐⭐⭐⭐
gmhelper专注国密,性能好基于 Bouncy Castle⭐⭐⭐
gmspring-boot-starterSpring Boot 集成,开箱即用参见 GitHub⭐⭐⭐⭐
优先使用 Hutool 国密工具

如果你的项目已经引入了 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 FileUtil

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);
        }
    }
}
成熟库替代:Hutool BeanUtil
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")); // 20240115
成熟库替代:Hutool DateUtil
import 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.x 推荐使用 RestClient

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
);
成熟库替代:Hutool HttpUtil
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 PinyinUtilHutool
国密加密自定义 SM4UtilHutool SM4 / Bouncy CastleHutool
文件操作java.nio.file.FilesHutool FileUtil / Commons IOHutool / Commons
Bean 转 Mapjava.beans.IntrospectorHutool BeanUtil / Commons BeanUtilsHutool
集合判空自定义 CollUtilHutool CollUtil / Commons CollectionsHutool
日期处理java.time APIHutool DateUtiljava.time(首选)/ Hutool
HTTP 调用RestClient / RestTemplate / OkHttpHutool HttpUtilSpring Boot 3.2+ 优先 RestClient
JSON 处理JacksonHutool JSONUtilJackson(Spring 项目)
Hutool 5.8.44 依赖汇总

只需引入需要的模块,不必全量引入:

<!-- 核心工具(字符串、日期、集合、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>

代码组织建议:

  1. 收集到 commonutil 模块中,统一管理
  2. 每个工具类加 @UtilityClass(Lombok)或私有构造函数,禁止实例化
  3. 工具方法都应该是 static 且无副作用
  4. 异常统一包装为 RuntimeException,或定义业务异常
  5. 写好 Javadoc,尤其是参数说明和使用示例
  6. 优先使用成熟库,只在成熟库不满足需求时手写

九、参考资料


系列导航: Java 工程化实战系列 | 上一篇:Java PDF 模板生成实战