目录

POI处理excel

流式读取excel

[xl]
  └── worksheets
        └── sheet1.xml   ← 这就是数据所在的地方!
        └── style.xml    ← 这就是样式所在的地方!
  └── sharedStrings.xml  ← 所有字符串集中存放在这里(节省空间)
[Content_Types].xml
<worksheet>
  <sheetData>
    <row r="1">
      <c r="A1" t="s"><v>0</v></c>   <!-- t="s" 表示是共享字符串,索引0 -->
      <c r="B1" t="s"><v>1</v></c>
    </row>
    <row r="2">
      <c r="A2"><v>123</v></c>       <!-- 没有 t="s",就是数字 -->
      <c r="F3">
        <f>D3/E3</f>
        <v>1.9166666666666667</v>
      </c>
      <c r="C2" t="e">               <!-- t="e",错误 -->
        <f>A2/(B2-2)</f>
        <v>#DIV/0!</v>
      </c>
      <c r="G1" t="str">             <!-- t="str" 内联字符串 -->
        <f>E1&amp;"@@"&amp;F1</f>
        <v>数据0-7@@数据0-8</v>
      </c>
      <c r="D1" t="b">               <!-- t="c" 布尔值 0(false)或 1(true) -->
        <v>1</v>
      </c>
      <c r="D2" s="4">               <!-- s="4" 样式索引,在style.xml 的 cellXfs 元素-->
        <v>45992</v>
      </c>
    </row>
  </sheetData>
</worksheet>
t 的值含义说明
(不存在)数字(Number)默认类型。包括整数、小数、日期(Excel 用数字表示日期)、时间等
"s"共享字符串(Shared String)值是 sharedStrings.xml 中的索引(整数),对应一个字符串
"str"内联字符串(Inline String)字符串直接写在 <v> 中,不经过 SST。常用于公式结果或单次出现的文本
"b"布尔值(Boolean)<v> 中为 0(false)或 1(true)
"e"错误值(Error)如 #N/A, #VALUE!, #DIV/0! 等。<v> 中是错误代码(如 #N/A)
"n"数字(Number)极少使用。规范允许但 Excel 通常不写这个(默认就是数字)
🔥warning
  • 特别注意:日期的坑!虽然日期在 XML 中也是 45000(数字),但:
    • 它没有特殊标记(不像字符串有 t="s");
    • 必须通过 单元格样式(CellStyle) 才知道它是日期;
    • 但在流式读取(Event API)中,拿不到 CellStyle!
    • 👉 所以:纯 Event API 无法 100% 区分“普通数字”和“日期”。
  • 解决方案(如果需要支持日期):
    • 业务约定:某几列一定是日期;
    • 结合 styles.xml 解析(复杂,POI 不直接支持,这非常复杂,POI 官方示例 XLSX2CSV 实现了这个逻辑。);
    • 改用 XSSFCell 读取(非流式) —— 但会失去内存优势。
  • Excel 中的日期是靠单元格格式来决定的,sheet1.xml 中对于日期会有一个格式属性:s="x"style.xml 中的 cellXfs 元素是样式,x 是样式索引,cellXfs 的子元素 xfnumFmtId 属性表示样式类型,其中 ID14~22 是内置日期格式
  • POI 的 Event API 提供了访问 styles.xml 的 API:reader.getStylesTable()
    • 解析 styles.xml
    • 建立 xfId → numFmtId 映射;
    • 再建立 numFmtId → 是否为日期格式的映射;
    • 在解析 sheet.xml 时,通过 <c s="5">s 属性(样式索引)查表。
<sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="8" uniqueCount="8">
    <si>
        <t>数据0-0</t>
    </si>
    <si>
        <t>数据0-1</t>
    </si>
    <si>
        <t>assa</t>
        <phoneticPr fontId="1" type="noConversion"/>
    </si>
</sst>
package com.zmy.excel;

import cn.hutool.core.util.StrUtil;
import org.apache.poi.openxml4j.opc.OPCPackage;
import org.apache.poi.openxml4j.opc.PackageAccess;
import org.apache.poi.ss.usermodel.DateUtil;
import org.apache.poi.xssf.eventusermodel.XSSFReader;
import org.apache.poi.xssf.model.SharedStringsTable;
import org.apache.poi.xssf.model.StylesTable;
import org.apache.poi.xssf.usermodel.XSSFCellStyle;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.SAXParserFactory;
import java.io.InputStream;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.regex.Pattern;

/**
 * @author zmy
 * @description 使用 SAX 解析 Excel 文件(支持日期自动识别)
 * @since 2025-12-13 21:13:35
 */
public class SimpleStreamingReader {

    private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    private static final String FEATURE_DISALLOW_DOCTYPE_DECL = "http://apache.org/xml/features/disallow-doctype-decl";

    public static void main(String[] args) throws Exception {
        String filePath = "E:\\opt\\TestData\\exportExcelWithXSSF.xlsx";
        List<List<String>> rows = parseExcelSheetByIndex(filePath, 2);
        // List<List<String>> rows = parseExcelSheetByName(filePath, "Sheet2");
        rows.forEach(System.out::println);
    }

    public static List<List<String>> parseExcelSheetByIndex(String filePath, int sheetIndex) {
        if (sheetIndex < 0) {
            throw new IllegalArgumentException("Sheet index must be >= 0");
        }

        try (OPCPackage pkg = OPCPackage.open(filePath, PackageAccess.READ)) {
            return parseExcelSheetByIndexInternal(pkg, sheetIndex);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private static List<List<String>> parseExcelSheetByIndexInternal(OPCPackage pkg, int sheetIndex) throws Exception {
        XSSFReader reader = new XSSFReader(pkg);
        SharedStringsTable sst = (SharedStringsTable) reader.getSharedStringsTable();
        StylesTable styles = reader.getStylesTable();

        SAXParserFactory spf = SAXParserFactory.newInstance();
        spf.setFeature(FEATURE_DISALLOW_DOCTYPE_DECL, true);
        XMLReader parser = spf.newSAXParser().getXMLReader();

        Iterator<InputStream> sheets = reader.getSheetsData();
        int currentIndex = 0;
        while (sheets.hasNext()) {
            try (InputStream sheet = sheets.next()) {
                if (currentIndex == sheetIndex) {
                    MyHandler handler = new MyHandler(sst, styles);
                    parser.setContentHandler(handler);
                    parser.parse(new InputSource(sheet));
                    return handler.getRows();
                }
                currentIndex++;
            }
        }
        throw new IllegalArgumentException("Sheet index out of range: " + sheetIndex);
    }

    public static List<List<String>> parseExcelSheetByName(String filePath, String sheetName) {
        try (OPCPackage pkg = OPCPackage.open(filePath, PackageAccess.READ)) {
            List<String> sheetNames = getSheetNames(pkg);
            int index = sheetNames.indexOf(sheetName);
            if (index == -1) {
                throw new IllegalArgumentException("Sheet not found: " + sheetName + ". Available: " + sheetNames);
            }
            return parseExcelSheetByIndexInternal(pkg, index);
        } catch (Exception e) {
            throw new RuntimeException("Failed to parse sheet by name", e);
        }
    }

    private static List<String> getSheetNames(OPCPackage pkg) throws Exception {
        // 找到 workbook.xml part
        try (InputStream wbStream = pkg.getPartsByName(Pattern.compile("/xl/workbook\\.xml")).get(0).getInputStream()) {
            DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
            dbf.setFeature(FEATURE_DISALLOW_DOCTYPE_DECL, true);

            DocumentBuilder db = dbf.newDocumentBuilder();
            Document doc = db.parse(wbStream);
            doc.getDocumentElement().normalize();

            NodeList sheetNodes = doc.getElementsByTagName("sheet");
            List<String> sheetNames = new ArrayList<>(sheetNodes.getLength());

            for (int i = 0; i < sheetNodes.getLength(); i++) {
                Element sheet = (Element) sheetNodes.item(i);
                String name = sheet.getAttribute("name");
                sheetNames.add(name);
            }
            return sheetNames;
        }
    }

    private static class MyHandler extends DefaultHandler {
        private final List<List<String>> allRows = new ArrayList<>(); // 存储所有行
        private final SharedStringsTable sst;
        private final StylesTable styles;
        private List<String> currentRow = null;
        private final StringBuilder currentValue = new StringBuilder();

        private String cellPosition = null;
        private String cellType = null;
        private String cellStyleIndex = null;
        private boolean insideV = false;
        private final boolean isExcelDateFormat;

        public MyHandler(SharedStringsTable sst, StylesTable styles) {
            this(sst, styles, false);
        }

        public MyHandler(SharedStringsTable sst, StylesTable styles, boolean isExcelDateFormat) {
            this.sst = sst;
            this.styles = styles;
            this.isExcelDateFormat = isExcelDateFormat;
        }

        public List<List<String>> getRows() {
            return allRows;
        }

        @Override
        public void startElement(String uri, String localName, String qName, Attributes attributes) {
            if ("row".equals(qName)) {
                // 获取行号(可选)attributes.getValue("r")
                currentRow = new ArrayList<>(); // 每行新建
            } else if ("c".equals(qName)) {
                if (currentRow == null) {
                    currentRow = new ArrayList<>(); // 安全兜底
                }
                cellPosition = attributes.getValue("r");
                cellType = attributes.getValue("t");
                cellStyleIndex = attributes.getValue("s");
                currentValue.setLength(0);
                insideV = false;
            } else if ("v".equals(qName)) {
                insideV = true; // ← 开始收集 <v> 的内容
            }
            // 注意:<f>(公式)我们完全忽略,不设置 insideV
        }

        // 遇到xml节点的文本(比如 <v> 的内容)时调用
        @Override
        public void characters(char[] ch, int start, int length) {
            if (insideV) {
                currentValue.append(ch, start, length); // ← 只收 <v> 的内容
            }
            // 忽略 <f>、<is> 等其他标签的文本
        }

        @Override
        public void endElement(String uri, String localName, String qName) {
            if ("v".equals(qName)) {
                insideV = false;
            } else if ("c".equals(qName)) {
                if (currentRow == null) {
                    return;
                }
                String value = currentValue.toString().trim();
                String finalValue;

                // 判断是否有实际内容(只有 <v> 存在才算有值)
                boolean hasValue = !value.isEmpty() || "s".equals(cellType) || "b".equals(cellType) || "e".equals(cellType);

                if (hasValue) {
                    finalValue = parseCellValue(value, cellType, cellStyleIndex);
                } else {
                    finalValue = ""; // 或 null,根据需求
                }

                // === 关键:按真实列位置插入 ===
                if (cellPosition != null) {
                    int colIndex = getColumnIndex(cellPosition);
                    // 确保 currentRow 足够长
                    while (currentRow.size() <= colIndex) {
                        currentRow.add(""); // 或 null
                    }
                    currentRow.set(colIndex, finalValue);
                } else {
                    // 极少数情况无 r(连续单元格),按顺序追加(不推荐,但兼容)
                    currentRow.add(finalValue);
                }

            } else if ("row".equals(qName)) {
                // 行结束:只输出包含非空值的行(可选)
                if (currentRow != null && currentRow.stream().anyMatch(StrUtil::isNotEmpty)) {
                    allRows.add(new ArrayList<>(currentRow)); // 深拷贝防篡改
                }
                currentRow = null;
            }
        }

        private String parseCellValue(String value, String cellType, String cellStyleIndex) {
            if ("s".equals(cellType)) {
                int idx = Integer.parseInt(value);
                return sst.getItemAt(idx).getString();
            } else if ("str".equals(cellType)) {
                return value;
            } else if ("b".equals(cellType)) {
                return "1".equals(value) ? "TRUE" : "FALSE";
            } else if ("e".equals(cellType)) {
                return parseErrorValue(value);
            } else {
                // 数字/日期
                if (value.isEmpty()) {
                    return "";
                }
                if (isDateFormatted(cellStyleIndex)) {
                    try {
                        double d = Double.parseDouble(value);

                        // 使用Excel中定义的日期格式对日期进行格式化
                        if (isExcelDateFormat) {
                            String excelFmtStr = styles.getStyleAt(Integer.parseInt(cellStyleIndex)).getDataFormatString();
                            return formatCellValueAsDate(d, excelFmtStr);
                        }
                        // 使用默认格式对日期格式化
                        Date date = DateUtil.getJavaDate(d, false);
                        return DATE_FORMAT.format(date);
                    } catch (Exception e) {
                        // fallback to raw value
                    }
                }
                return value;
            }
        }

        // 新增:判断是否为日期格式
        private boolean isDateFormatted(String cellStyleIndex) {
            if (styles == null || cellStyleIndex == null || cellStyleIndex.isEmpty()) {
                return false;
            }
            try {
                int xfIndex = Integer.parseInt(cellStyleIndex);
                XSSFCellStyle style = styles.getStyleAt(xfIndex);
                if (style == null) {
                    return false;
                }
                short fmtId = style.getDataFormat();
                String fmtStr = style.getDataFormatString();
                return DateUtil.isADateFormat(fmtId, fmtStr);
            } catch (Exception ignored) {
                return false;
            }
        }

        // === 工具方法:从 "AB123" 提取列索引(0-based)===
        private static int getColumnIndex(String cellRef) {
            if (cellRef == null || cellRef.isEmpty()) {
                return 0;
            }

            int i = 0;
            while (i < cellRef.length() && Character.isLetter(cellRef.charAt(i))) {
                i++;
            }
            if (i == 0) {
                return 0; // 无列标识,默认第0列
            }

            String col = cellRef.substring(0, i).toUpperCase();
            int result = 0;
            for (char c : col.toCharArray()) {
                result = result * 26 + (c - 'A' + 1);
            }
            return result - 1; // 转为 0-based
        }

        // 错误值映射表(Excel 错误码 -> 字符串)
        private static final Map<Integer, String> ERROR_MAP = Map.of(
                0, "#NULL!",
                7, "#DIV/0!",
                15, "#VALUE!",
                23, "#REF!",
                29, "#NAME?",
                36, "#NUM!",
                42, "#N/A"
        );

        private static final Set<String> VALID_ERROR_STRINGS = Set.of("#NULL!", "#DIV/0!", "#VALUE!", "#REF!", "#NAME?", "#NUM!", "#N/A");

        private String parseErrorValue(String value) {
            if (value == null) {
                return "#ERROR!";
            }

            // 情况 1: 已经是标准错误字符串(如 "#N/A")
            if (VALID_ERROR_STRINGS.contains(value)) {
                return value;
            }

            // 情况 2: 是数字错误码(如 "42")
            try {
                int code = Integer.parseInt(value.trim());
                return ERROR_MAP.getOrDefault(code, "#ERROR!");
            } catch (NumberFormatException e) {
                // 情况 3: 无法识别的内容(如公式文本混入)
                return "#ERROR!";
            }
        }
    }


    // 日期格式转换
    private static String formatCellValueAsDate(double excelDate, String excelFormatStr) {
        // 先转为 java.util.Date
        Date date = DateUtil.getJavaDate(excelDate, false);

        if (excelFormatStr == null || excelFormatStr.trim().isEmpty()) {
            // 没有自定义格式,用默认
            return DATE_FORMAT.format(date);
        }

        // 尝试将 Excel 格式转换为 Java 格式
        String javaPattern = convertExcelDateFormatToJava(excelFormatStr);
        if (javaPattern != null) {
            try {
                return cn.hutool.core.date.DateUtil.format(date, javaPattern);
            } catch (Exception e) {
                // 转换失败,降级
            }
        }

        // 最终 fallback
        return DATE_FORMAT.format(date);
    }

    // Excel 日期格式转换
    private static String convertExcelDateFormatToJava(String excelFmt) {
        if (excelFmt == null) {
            return null;
        }

        String fmt = excelFmt.trim();

        // 移除颜色、条件等非格式部分(简单处理)
        fmt = fmt.replaceAll("\\[.*?]", ""); // 移除 [Red], [>=100] 等

        // 替换转义字符(Excel 用 \ 转义,如 hh\:mm)
        fmt = fmt.replace("\\:", ":");

        // 按顺序替换(长匹配优先)
        // 注意:必须从长到短,避免 "mmmm" 被先替成 "mm"
        fmt = fmt.replace("mmmm", "MMMM") // January
                .replace("mmm", "MMM")    // Jan
                .replace("dddd", "EEEE")  // Monday
                .replace("ddd", "EEE")    // Mon
                .replace("hh", "HH")
                .replace("h", "H");

        // 处理 "mm" 和 "m" —— 这是最 tricky 的部分
        // mm 在 Excel 中既可表示“月”也可表示“分”,但在 日期格式上下文中,通常:
        // 出现在 yyyy-mm-dd → 是“月”
        // 出现在 hh:mm → 是“分”
        // 简单策略:如果包含 "HH" 或 "H",则 "mm" 视为分钟;否则视为月份
        if (!fmt.contains("HH") && !fmt.contains("H")) {
            fmt = fmt.replace("mm", "MM").replace("m", "M"); // 月份
        }

        // 验证是否为有效 Java 格式(可选)
        try {
            new SimpleDateFormat(fmt);
            return fmt;
        } catch (Exception e) {
            return null; // 无效,返回 null 让 caller 用默认
        }
    }

}

DOM 模式读写,清理空行空列

import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;

import java.io.*;
import java.util.ArrayList;
import java.util.List;

/**
 * @author zmy
 * @description Row.MissingCellPolicy 枚举中的三个选项用于控制当获取不存在的单元格时的行为:
 * CREATE_NULL_AS_BLANK
 * 当单元格不存在时,创建一个新的空白单元格并返回
 * 适用于需要确保始终获得有效 Cell 对象的场景
 * 推荐用于现代 POI 版本
 * <p>
 * RETURN_BLANK_AS_NULL
 * 如果单元格存在但为空白,则返回 null
 * 如果单元格不存在,也返回 null
 * 区分不了"空白单元格"和"不存在的单元格"
 * <p>
 * RETURN_NULL_AND_BLANK
 * 如果单元格不存在,返回 null
 * 如果单元格存在但为空白,返回该空白单元格
 * 能够区分"不存在"和"空白"两种状态
 * <p>
 * 大多数情况下推荐使用 CREATE_NULL_AS_BLANK,避免空指针异常
 * 如需区分空白和不存在的单元格,可使用 RETURN_NULL_AND_BLANK
 * <p>
 * removeRow()的真实行为,执行后:
 * ✅ 移除该行的所有单元格数据
 * ✅ 将该行对象置为null
 * ❌ 不重新排列其他行的位置
 * ❌ 不减少总行数(getLastRowNum()不变)
 * @since 2025-12-13 10:51:52
 */
@Slf4j
public class ExcelEmptyCellProcessor {

    /**
     * 清理Excel文件(删除所有工作表的空行空列)
     * 用完后请调用 Workbook.close() 关闭资源
     * @param inputFile 输入文件
     * @return 清理后的工作簿
     */
    public static Workbook cleanExcelFile(File inputFile) throws IOException {
        try (Workbook orignalWorkbook = WorkbookFactory.create(inputFile)) {
            Workbook workbook = createExactBackup(orignalWorkbook);
            // 转换所有公式为值
            convertAllFormulasToValues(workbook);
            // 清理工作簿
            cleanExcelWorkbook(workbook);
            return workbook;
        }
    }

    /**
     * 清理整个Excel文件(删除所有工作表的空行空列)
     *
     * @param inputFile  输入文件
     * @param outputFile 输出文件
     */
    public static void cleanExcelFile(File inputFile, File outputFile) throws IOException {
        try (Workbook workbook = cleanExcelFile(inputFile);
             FileOutputStream out = new FileOutputStream(outputFile)) {
            log.info("开始写入Excel文件...");
            workbook.write(out);
        }
    }

    /**
     * 清理工作簿中的所有空行空列
     *
     * @param workbook 工作簿
     */
    public static void cleanExcelWorkbook(Workbook workbook) {
        if (workbook == null) {
            return;
        }

        for (int i = 0; i < workbook.getNumberOfSheets(); i++) {
            Sheet sheet = workbook.getSheetAt(i);
            if (sheet != null) {
                cleanSheet(sheet);
            }
        }
    }

    /**
     * 清理单个工作表
     *
     * @param sheet 工作表
     */
    public static void cleanSheet(Sheet sheet) {
        if (sheet == null) return;
        removeEmptyRows(sheet);
        removeEmptyColumns(sheet);
    }

    /**
     * 创建一个精确的备份,用于恢复原始工作簿
     *
     * @param original 原始工作簿
     * @return 精确备份工作簿
     */
    public static Workbook createExactBackup(Workbook original) {
        try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
            // 方法1:通过内存序列化创建精确副本
            original.write(bos);

            try (ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray())) {
                return WorkbookFactory.create(bis);
            }

        } catch (IOException e) {
            throw new RuntimeException("创建备份失败", e);
        }
    }

    /**
     * 转换所有公式为值
     *
     * @param workbook 工作簿
     */
    public static void convertAllFormulasToValues(Workbook workbook) {
        for (int sheetIndex = 0; sheetIndex < workbook.getNumberOfSheets(); sheetIndex++) {
            Sheet sheet = workbook.getSheetAt(sheetIndex);
            convertSheetFormulasToValues(sheet);
        }
    }

    public static void convertSheetFormulasToValues(Sheet sheet) {
        for (Row row : sheet) {
            if (row != null) {
                convertRowFormulasToValues(row);
            }
        }
    }

    /**
     * 遍历行中的所有单元格,将公式转换为值
     *
     * @param row 行对象
     */
    public static void convertRowFormulasToValues(Row row) {
        for (Cell cell : row) {
            if (cell != null && cell.getCellType() == CellType.FORMULA) {
                Object cellValue = evaluateFormulaCellValue(cell);

                // 移除公式定义,使单元格变为普通单元格
                cell.removeFormula();

                if (cellValue == null) {
                    cell.setBlank(); // 设置为空白单元格
                } else if (cellValue instanceof String s) {
                    cell.setCellValue(s);
                } else if (cellValue instanceof Boolean b) {
                    cell.setCellValue(b);
                } else if (cellValue instanceof Number n) {
                    cell.setCellValue(n.doubleValue()); // POI 要求 double
                } else {
                    // 理论上不会走到这里,保险起见设为空
                    cell.setBlank();
                }
            }
        }
    }

    /**
     * 判断行是否为空行
     * getFirstCellNum() 返回第一个有效单元格的索引,从0开始
     * getLastCellNum() 返回该行单元格的最后一个单元格的索引+1,表示该行中单元格的数量
     *
     * @param row 行对象
     * @return true-空行, false-非空行
     */
    public static boolean isEmptyRow(Row row) {
        if (row == null) {
            return true;
        }

        // 遍历行中的所有单元格
        for (int cellNum = row.getFirstCellNum(); cellNum < row.getLastCellNum(); cellNum++) {
            Cell cell = row.getCell(cellNum, Row.MissingCellPolicy.CREATE_NULL_AS_BLANK);
            if (isNotEmptyCell(cell)) {
                return false;
            }
        }
        return true;
    }

    /**
     * 判断列是否为空列
     *
     * @param sheet       工作表
     * @param columnIndex 列索引
     * @return true-空列, false-非空列
     */
    public static boolean isEmptyColumn(Sheet sheet, int columnIndex) {
        if (sheet == null) {
            return true;
        }

        int firstRow = sheet.getFirstRowNum();
        int lastRow = sheet.getLastRowNum();

        for (int rowNum = firstRow; rowNum <= lastRow; rowNum++) {
            Row row = sheet.getRow(rowNum);
            if (row != null) {
                Cell cell = row.getCell(columnIndex, Row.MissingCellPolicy.CREATE_NULL_AS_BLANK);
                if (isNotEmptyCell(cell)) {
                    return false;
                }
            }
        }
        return true;
    }

    /**
     * 判断单元格是否非空
     *
     * @param cell 单元格对象
     * @return false-空单元格, true-非空单元格
     */
    public static boolean isNotEmptyCell(Cell cell) {
        if (cell == null || cell.getCellType() == CellType.BLANK) {
            return false;
        }

        switch (cell.getCellType()) {
            case STRING:
                // Apache POI 的 getStringCellValue() 对非字符串单元格会抛异常
                return !cell.getStringCellValue().trim().isEmpty();
            case NUMERIC:
                return true; // 数字类型不为空
            case BOOLEAN:
                return true; // 布尔类型不为空
            case FORMULA:
                try {
                    // 尝试计算公式的值
                    Object cellValue = evaluateFormulaCellValue(cell);
                    if (cellValue == null) {
                        return false;
                    } else if (cellValue instanceof String s) {
                        return !s.trim().isEmpty();
                    } else {
                        // Number, Boolean, etc. — 都视为非空
                        return true;
                    }
                } catch (Exception e) {
                    return false; // 公式无法计算,视为“空”或“无效”
                }
            default:
                return false;
        }
    }


    /**
     * 计算公式单元格的值
     */
    private static Object evaluateFormulaCellValue(Cell cell) {
        if (cell == null) {
            return null;
        }
        Workbook workbook = cell.getSheet().getWorkbook();
        FormulaEvaluator evaluator = workbook.getCreationHelper().createFormulaEvaluator();
        CellValue cellValue = evaluator.evaluate(cell);

        if (cellValue == null) {
            return null;
        }

        return switch (cellValue.getCellType()) {
            case NUMERIC -> cellValue.getNumberValue();
            case STRING -> cellValue.getStringValue();
            case BOOLEAN -> cellValue.getBooleanValue();
            case BLANK -> null;   // 空白值视为 null
            case ERROR -> null;   // 错误值视为 null
            default -> null;
        };
    }


    /**
     * 删除工作表中的空行
     *
     * @param sheet 工作表
     */
    public static void removeEmptyRows(Sheet sheet) {
        if (sheet == null) {
            return;
        }

        List<Integer> emptyRowIndexes = new ArrayList<>();
        int firstRow = sheet.getFirstRowNum();
        int lastRow = sheet.getLastRowNum();

        // 收集空行索引
        for (int rowNum = firstRow; rowNum <= lastRow; rowNum++) {
            Row row = sheet.getRow(rowNum);
            if (isEmptyRow(row)) {
                emptyRowIndexes.add(rowNum);
            }
        }

        // 从后往前删除(避免索引变化)
        for (int i = emptyRowIndexes.size() - 1; i >= 0; i--) {
            int rowNum = emptyRowIndexes.get(i);
            Row row = sheet.getRow(rowNum);
            if (row != null) {
                sheet.removeRow(row);
            }
            // 将之后的行上移1行
            if (rowNum < sheet.getLastRowNum()) {
                sheet.shiftRows(rowNum + 1, sheet.getLastRowNum(), -1);
            }
        }

    }

    /**
     * 删除工作表中的空列
     *
     * @param sheet 工作表
     */
    public static void removeEmptyColumns(Sheet sheet) {
        if (sheet == null) {
            return;
        }

        List<Integer> emptyColumnIndexes = new ArrayList<>();
        int maxColumns = getMaxColumnCount(sheet);

        // 收集空列索引
        for (int colNum = 0; colNum < maxColumns; colNum++) {
            if (isEmptyColumn(sheet, colNum)) {
                emptyColumnIndexes.add(colNum);
            }
        }

        // 从后往前删除(避免索引变化)
        for (int i = emptyColumnIndexes.size() - 1; i >= 0; i--) {
            int colNum = emptyColumnIndexes.get(i);
            removeColumn(sheet, colNum);
        }

    }

    /**
     * 获取工作表中的最大列数
     */
    private static int getMaxColumnCount(Sheet sheet) {
        int maxColumns = 0;
        for (Row row : sheet) {
            if (row != null && row.getLastCellNum() > maxColumns) {
                maxColumns = row.getLastCellNum();
            }
        }
        return maxColumns;
    }

    /**
     * 删除指定列
     */
    private static void removeColumn(Sheet sheet, int columnIndex) {
        for (Row row : sheet) {
            if (row != null) {
                Cell cell = row.getCell(columnIndex);
                if (cell != null) {
                    row.removeCell(cell);
                }

                // 将右侧的单元格左移
                for (int i = columnIndex + 1; i <= row.getLastCellNum(); i++) {
                    Cell rightCell = row.getCell(i);
                    if (rightCell != null) {
                        Cell newCell = row.createCell(i - 1);
                        copyCellStyle(newCell, rightCell);
                        setCellValue(newCell, rightCell);
                    }
                }
                row.removeCell(row.getCell(row.getLastCellNum() - 1));
            }
        }
    }

    /**
     * 复制单元格样式
     */
    private static void copyCellStyle(Cell target, Cell source) {
        if (source == null || target == null) return;

        try {
            // 检查源单元格是否有样式
            CellStyle sourceStyle = source.getCellStyle();
            if (sourceStyle != null) {
                CellStyle newStyle = target.getSheet().getWorkbook().createCellStyle();
                newStyle.cloneStyleFrom(sourceStyle);
                target.setCellStyle(newStyle);
            }
        } catch (Exception e) {
            // 忽略样式复制过程中的异常,避免因XML对象断开连接导致程序中断
            // 可以选择记录日志或静默处理
            log.error("复制单元格样式时发生错误: {}", e.getMessage(), e);
        }
    }

    /**
     * 设置单元格值
     */
    private static void setCellValue(Cell target, Cell source) {
        if (source == null || target == null) {
            return;
        }

        switch (source.getCellType()) {
            case STRING:
                target.setCellValue(source.getStringCellValue());
                break;
            case NUMERIC:
                target.setCellValue(source.getNumericCellValue());
                break;
            case BOOLEAN:
                target.setCellValue(source.getBooleanCellValue());
                break;
            case FORMULA:
                target.setCellFormula(source.getCellFormula());
                break;
            default:
                break;
        }
    }

    /**
     * 使用 SXSSFWorkbook 导出 Excel 文件,SXSSF模式(流式写)
     *
     * @param filePath 文件路径
     */
    public static void exportExcelWithXSSF(String filePath) {
        long start = System.currentTimeMillis();

        // 使用 try-with-resources 确保资源释放,内存保持100行数据
        try (SXSSFWorkbook workbook = new SXSSFWorkbook(100)) {
            Sheet sheet = workbook.createSheet("Sheet1");

            for (int i = 0; i < 1_000_000; i++) {
                Row row = sheet.createRow(i);
                for (int j = 0; j < 10; j++) {
                    row.createCell(j).setCellValue(i + "-" + j);
                }
            }

            // 写入文件
            try (FileOutputStream out = new FileOutputStream(filePath)) {
                workbook.write(out);
                out.flush(); // 强制刷出缓冲区
            }

            log.info("Excel 文件已写入: {}, 耗时: {} ms", filePath, System.currentTimeMillis() - start);

        } catch (Exception e) {
            log.error("导出 Excel 失败", e);
            throw new RuntimeException(e);
        }
        // ✅ SXSSFWorkbook 在 close() 时会自动 dispose(),无需手动调用
    }
}

回到目录

POI处理word

中文字号磅值 (pt)OOXML 底层 w:val (半磅)
初号42pt84
小初36pt72
一号26pt52
小一24pt48
二号22pt44
小二18pt36
三号16pt32
小三15pt30
四号14pt28
小四12pt24
五号10.5pt21
小五9pt18
六号7.5pt15
小六6.5pt13
七号5.5pt11
八号5pt10

主要类及其含义

XWPFDocument doc = new XWPFDocument(); // 创建新文档
XWPFDocument doc = new XWPFDocument(inputStream); // 从输入流读取
doc.write(outputStream); // 写入输出流
List<IBodyElement> allElements = document.getBodyElements(); // 获取所有元素
List<XWPFParagraph> paragraphs = doc.getParagraphs(); // 获取所有段落
List<XWPFTable> tables = doc.getTables(); // 获取所有表格
XWPFWordExtractor extractor = new XWPFWordExtractor(document));
// 获取完整文本
String fullText = extractor.getText();
// extractor.getText() 源码,XWPFHeaderFooterPolicy 表示页眉页脚的策略,奇偶页等
public String getText() {
    StringBuilder text = new StringBuilder(64);
    XWPFHeaderFooterPolicy hfPolicy = document.getHeaderFooterPolicy();

    // Start out with all headers
    extractHeaders(text, hfPolicy);

    // Process all body elements
    for (IBodyElement e : document.getBodyElements()) {
        appendBodyElementText(text, e);
        text.append('\n');
    }

    // Finish up with all the footers
    extractFooters(text, hfPolicy);

    return text.toString();
}
XWPFParagraph para = doc.createParagraph(); // 创建段落
document.getPosOfParagraph(para);  // 获取段落所在的位置
para.setAlignment(ParagraphAlignment.CENTER); // 设置对齐方式
para.setSpacingBefore(10); // 设置段前间距
para.setSpacingAfter(10); // 设置段后间距
XWPFRun run = para.createRun(); // 创建文本运行
String text = para.getText(); // 获取段落文本
XWPFRun run = para.createRun();
run.setText("文本内容");
run.setBold(true); // 加粗
run.setItalic(true); // 斜体
run.setFontSize(24); // 字体大小(24=12pt)
run.setFontFamily("宋体"); // 字体
run.setColor("FF0000"); // 字体颜色
run.addBreak(); // 添加换行
run.addTab(); // 添加制表符
XWPFTable table = doc.createTable(); // 创建表格
table.setWidth("100%"); // 设置表格宽度

// 获取/创建行
XWPFTableRow row = table.getRow(0); // 获取第一行
XWPFTableRow newRow = table.createRow(); // 创建新行

// 获取/创建单元格
XWPFTableCell cell = row.getCell(0); // 获取第一个单元格
XWPFTableCell newCell = row.createCell(); // 创建新单元格

// 设置表格样式
CTTblPr tblPr = table.getCTTbl().getTblPr();
tblPr.addNewTblW().setW(BigInteger.valueOf(5000)); // 设置宽度
cell.setText("单元格内容");
cell.setColor("FFFFFF"); // 设置背景色
cell.setVerticalAlignment(XWPFTableCell.XWPFVertAlign.CENTER); // 垂直对齐

// 在单元格中添加段落
XWPFParagraph cellPara = cell.addParagraph();
XWPFRun cellRun = cellPara.createRun();
cellRun.setText("单元格中的文本");
XWPFStyles styles = doc.getStyles();
String styleId = para.getStyleID();
String styleId = run.getStyle();
XWPFStyle style = styles.getStyle(styleId); // 根据样式ID获取样式
XWPFStyle newStyle = styles.newStyle("MyStyle"); // 创建新样式
XWPFHeader header = doc.createHeader(HeaderFooterType.DEFAULT);
XWPFParagraph headerPara = header.createParagraph();
headerPara.createRun().setText("页眉内容");

XWPFFooter footer = doc.createFooter(HeaderFooterType.DEFAULT);
XWPFParagraph footerPara = footer.createParagraph();
footerPara.createRun().setText("页脚内容");
XWPFNumbering numbering = doc.getNumbering();
CTAbstractNum abstractNum = CTAbstractNum.Factory.newInstance();
// 创建超链接
CTHyperlink cthyper = paragraph.getCTP().addNewHyperlink();
cthyper.setId(hyperlink.getId());
XWPFHyperlink link = new XWPFHyperlink(cthyper);
CTP ctP = paragraph.getCTP(); // 获取底层XML对象
CTPPr pPr = ctP.getPPr(); // 获取段落属性
pPr.getJc(); // 对齐方式
pPr.getSpacing(); // 间距
pPr.getInd(); // 缩进

CTR ctR = run.getCTR(); // 获取底层XML对象
CTRPr rPr = ctR.getRPr(); // 获取运行属性
rPr.getB(); // 加粗
rPr.getI(); // 斜体
rPr.getSz(); // 字体大小
rPr.getRFonts(); // 字体
rPr.getColor(); // 颜色
public static void addParagraphAtBeginning(XWPFDocument doc, String text) {
    // 1. 获取文档的CTBody
    CTBody body = doc.getDocument().getBody();

    // 2. 在body开头插入新的CTP
    CTP newCTP = body.insertNewP(0);

    // 3. 创建段落对象
    XWPFParagraph newPara = new XWPFParagraph(newCTP, doc);

    // 4. 添加内容
    XWPFRun run = newPara.createRun();
    run.setText(text);
    run.setBold(true);
}

回到目录

FastExcel

@Slf4j
public class FastExcelTest {
    private final static String READ_FILENAME = "E:\\test\\事件集.xlsx";
    private final static String WRITE_FILENAME = "E:\\test\\写示例.xlsx";

    public static void main(String[] args) {
        List<DemoMergeData> list = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            DemoMergeData data = new DemoMergeData();
            data.setString("字符串" + i);
            data.setDate(new Date());
            data.setDoubleData(0.56+i);
            list.add(data);
        }
        FastExcel.write(WRITE_FILENAME, DemoMergeData.class)
                .sheet("合并示例")
                .doWrite(list);
    }
    public void simpleRead() {
        // 使用方式1:Lambda表达式直接处理数据
        FastExcel.read(READ_FILENAME, EventData.class, new PageReadListener<EventData>(dataList -> {
            for (EventData eventData : dataList) {
                log.info("读取到一条数据: {}", JSONUtil.toJsonStr(eventData));
            }
        })).sheet().doRead();

        // 使用方式2:匿名内部类
        FastExcel.read(READ_FILENAME, EventData.class, new ReadListener<EventData>() {
            @Override
            public void invoke(EventData data, AnalysisContext context) {
                log.info("读取到一条数据: {}", JSONUtil.toJsonStr(data));
            }
            @Override
            public void doAfterAllAnalysed(AnalysisContext context) { }
        }).sheet().doRead();

        // 使用方式3:自定义监听器
        // 定义pojo
        FastExcel.read(READ_FILENAME, EventData.class, new EventDataListener()).sheet().doRead();
        // 不定义pojo
        FastExcel.read(READ_FILENAME, new NoModelDataListener()).sheet().doRead();

        // 使用方式4:多 Sheet 读取
        try (ExcelReader excelReader = FastExcel.read(READ_FILENAME, EventData.class, new EventDataListener()).build()) {
            ReadSheet readSheet = FastExcel.readSheet(0).build();
            excelReader.read(readSheet);
        }
        // 使用方式5:同步读取,使用 doReadSync 方法直接将 Excel 数据读取为内存中的列表,这种方法适用于数据量较小的场景
        // 使用 FastExcel 同步读取 Excel 数据为对象列表
        List<EventData> listData = FastExcel.read(READ_FILENAME).head(EventData.class).sheet().doReadSync();
        log.info("读取到所有数据: {}", JSONUtil.toJsonStr(listData));

        // 直接读取为 Map 列表
        List<Map<Integer, String>> listMap = FastExcel.read(READ_FILENAME).sheet().doReadSync();
        log.info("读取到所有数据: {}", JSONUtil.toJsonStr(listMap));

        // 写法1:使用 Lambda 表达式分页获取数据
        FastExcel.write(WRITE_FILENAME, EventData.class)
                .sheet("模板")
                .doWrite(FastExcelTest::data);

        // 写法2:直接传递数据列表
        FastExcel.write(WRITE_FILENAME, EventData.class)
                .sheet("模板")
                .doWrite(data());

        // 写法3:使用 ExcelWriter 对象
        try (ExcelWriter excelWriter = FastExcel.write(READ_FILENAME, EventData.class).build()) {
            WriteSheet writeSheet = FastExcel.writerSheet("模板").build();
            excelWriter.write(data(), writeSheet);
        }
    }
    // 填充要写入的数据
    private static List<EventData> data() {
        List<EventData> list = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            EventData data = new EventData();
            list.add(data);
        }
        return list;
    }
    // 定义pojo、定义监听器读取一个简单的 Excel 文件
    public static class EventDataListener implements ReadListener<EventData> {

        @Override
        public void invoke(EventData data, AnalysisContext context) {
            log.info("解析到一条数据:{}", JSONUtil.toJsonStr(data));
        }

        @Override
        public void doAfterAllAnalysed(AnalysisContext context) {
            log.info("所有数据解析完成!");
        }
    }

    // 不定义POJO读取一个简单的 Excel 文件
    public static class NoModelDataListener extends AnalysisEventListener<Map<Integer, String>> {
        private static final int BATCH_COUNT = 5;
        private final List<Map<Integer, String>> cachedDataList = new ArrayList<>(BATCH_COUNT);

        @Override
        public void invoke(Map<Integer, String> data, AnalysisContext context) {
            log.info("解析到一条数据: {}", JSONUtil.toJsonStr(data));
            cachedDataList.add(data);
            if (cachedDataList.size() >= BATCH_COUNT) {
                saveData();
                cachedDataList.clear();
            }
        }

        @Override
        public void doAfterAllAnalysed(AnalysisContext context) {
            // 确保全部数据被处理
            saveData();
        }

        private void saveData() {
            // 实际业务处理逻辑
            log.info("存储 {} 条数据", Optional.of(cachedDataList.size()));
        }
    }
}
package com.zmy.excel;

import cn.idev.excel.converters.Converter;
import cn.idev.excel.converters.ReadConverterContext;
import cn.idev.excel.converters.WriteConverterContext;
import cn.idev.excel.enums.CellDataTypeEnum;
import cn.idev.excel.metadata.data.WriteCellData;

/**
 * @author zmy
 * @description EventTypeEnum 转换器,T 是泛型,表示支持的数据类型。
 * @since 2025-04-18 23:55:49
 */

public class CustomConvert implements Converter<EventTypeEnum> {
    @Override
    public Class<?> supportJavaTypeKey() {
        // 支持的 Java 类型
        return EventTypeEnum.class;
    }

    @Override
    public CellDataTypeEnum supportExcelTypeKey() {
        // 转换后的 Excel 数据类型
        return CellDataTypeEnum.NUMBER;
    }

    /*
    // 读取转换:Excel单元格 -> Java对象
    @Override
    public EventTypeEnum convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty,
                                           GlobalConfiguration globalConfiguration) {
        return EventTypeEnum.codeOf(Integer.valueOf(cellData.getStringValue()));
    }

    // 写入转换:Java对象 -> Excel单元格
    @Override
    public WriteCellData<?> convertToExcelData(EventTypeEnum value, ExcelContentProperty contentProperty,
                                               GlobalConfiguration globalConfiguration) {
        return new WriteCellData<>(String.valueOf(value.getCode()));
    }*/

    @Override
    public EventTypeEnum convertToJavaData(ReadConverterContext<?> context) {
        return EventTypeEnum.codeOf(Integer.valueOf(context.getReadCellData().getStringValue()));
    }

    // 写到excel中的都转为String,写为WriteCellData<String>
    @Override
    public WriteCellData<String> convertToExcelData(WriteConverterContext<EventTypeEnum> context) {
        return new WriteCellData<>(String.valueOf(context.getValue().getCode()));
    }
}

回到目录

CompletableFuture

@Slf4j
public class CompletableFutureTest {
    // 提交有返回值的异步任务
    public static void supplyAsync() throws ExecutionException, InterruptedException {
        // 创建一个CompletableFuture对象,异步执行任务
        CompletableFuture<Integer> task = CompletableFuture.supplyAsync(() -> {
            long start = System.currentTimeMillis();
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                log.error("执行任务失败", e);
            }
            // 打印任务执行时间
            System.out.println("执行任务耗时:" + (System.currentTimeMillis() - start) + "ms");
            return 1;
        });
        System.out.println("主线程正在运行。。。");
        System.out.println("任务是否完成:" + task.isDone());
        // join() 抛出非受检异常(CompletionException,包装原始异常),无需显式捕获。
        System.out.println("task.join()任务执行结果:" + task.join());
        // get():抛出受检异常(ExecutionException, InterruptedException),需要显式捕获或声明抛出。
        System.out.println("task.get()任务执行结果:" + task.get());
    }

    // 提交无返回值的异步任务
    public static void runAsync() {
        CompletableFuture<Void> task = CompletableFuture.runAsync(() -> {
            long start = System.currentTimeMillis();
            // 默认将任务提交到ForkJoinPool.commonPool()线程池中执行
            System.out.println(Thread.currentThread().getName() + "开始工作了,执行时间:" + start);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                log.error("执行任务失败", e);
            }
            System.out.println(Thread.currentThread().getName() + "结束工作了,总执行时间:" + (System.currentTimeMillis() - start) + "ms");
        });
        System.out.println("任务是否完成:" + task.isDone());
        System.out.println("任务执行结果:" + task.join());
    }

    // 将异步任务提交给自己的线程池处理
    public static void supplyAsyncWithExecutor() throws ExecutionException, InterruptedException {
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        CompletableFuture<Integer> task = CompletableFuture.supplyAsync(() -> {
            long start = System.currentTimeMillis();
            System.out.println(Thread.currentThread() + "开始工作了,执行时间:" + start);
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                log.error("执行任务失败", e);
            }
            // 打印当前执行任务的线程
            System.out.println(Thread.currentThread() + "结束工作了,总执行时间:" + (System.currentTimeMillis() - start));
            return 1;
        }, executorService);
        System.out.println("主线程开始运行");
        System.out.println("输出结果 " + task.get());
        System.out.println("主线程运行结束");

        executorService.shutdown();
        System.out.println("任务执行结束");
    }

    // thenApply 和 thenApplyAsync, 将第一个任务的返回值交给第二个异步任务处理
    // thenApply 在第一个异步任务有指定线程池的情况下,第二个异步任务会被提交到相同的线程池中,
    // thenApplyAsync 与 thenApply 不同的是,在第一个异步任务有指定线程池的情况下,第二个异步任务会被提交到其他线程池中,
    // 所以这里我们可以说明一个规律,带有 Async 关键字的方法支持组合任务时,将任务提交到不同的线程池中。
    // supply 方法是有入参有返回值,accept 方法是有入参无返回值,run 方法是无入参无返回值。
    public static void thenApply() {
        int s = 123;
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        CompletableFuture<Integer> task1 = CompletableFuture.supplyAsync(() -> {
            System.out.println("第一个线程:" + Thread.currentThread() + "开始工作了");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                log.error("执行任务失败", e);
            }
            System.out.println("第一个线程:" + Thread.currentThread() + "结束工作了");
            return s;
        }, executorService);
        CompletableFuture<String> task2 = task1.thenApply(result -> {
            System.out.println("第二个线程:" + Thread.currentThread() + "开始工作了");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                log.error("执行任务失败", e);
            }
            System.out.println("第二个线程:" + Thread.currentThread() + "结束工作了");
            return "第一个线程的结果是:" + result;
        });
        CompletableFuture<String> task3 = task2.thenApplyAsync(result -> {
            System.out.println("第三个线程:" + Thread.currentThread() + "开始工作了");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                log.error("执行任务失败", e);
            }
            System.out.println("第三个线程:" + Thread.currentThread() + "结束工作了");
            return "第二个线程的结果是:【" + result + "】";
        });
        executorService.shutdown();
        System.out.println("第三个线程的结果是:【" + task3.join() + "】");
    }

    // thenApplyAsync
    public static void thenAcceptAndRun() throws ExecutionException, InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        CompletableFuture<Integer> task = CompletableFuture.supplyAsync(() -> {
            System.out.println("task线程:" + Thread.currentThread().getName() + "开始工作了");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                log.error("执行任务失败", e);
            }
            System.out.println("task线程:" + Thread.currentThread().getName() + "结束工作了");
            return 200;
        }, executorService);

        CompletableFuture<Integer> task2 = task.thenApply((data) -> {
            System.out.println("task2线程:" + Thread.currentThread().getName() + "开始工作了");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                log.error("执行任务失败", e);
            }
            System.out.println("task2线程:" + Thread.currentThread().getName() + "执行结束");
            return data;
        });

        // thenAccept 收上一个任务的入参,但无返回值
        CompletableFuture<Void> task3 = task2.thenAccept((data) -> {
            System.out.println("task3线程: " + Thread.currentThread().getName() + ",该任务接收上一个任务的结果,但无返回值,收到上一个任务的结果值为 " + data);
        });

        // thenRun在上一个任务结束后执行,既无入参也无出参
        CompletableFuture<Void> task4 = task3.thenRun(() -> {
            System.out.println("task4在上一个任务结束后继续执行,无入参,也无返回值");
        });


        System.out.println("尝试获取最终执行结果");
        task4.get();
        System.out.println("执行任务直至task4 ");
        System.out.println("任务全部执行结束");

        executorService.shutdown();
    }

    // exceptionally
    public static void exceptionally() throws ExecutionException, InterruptedException {
        CompletableFuture<Integer> task = CompletableFuture.supplyAsync(() -> {
            System.out.println("task1 开始工作了");
            // 随机生成被除数,为0会抛出算术异常
            int num = RandomUtil.randomInt(0, 2);
            int result = 10 / num;
            System.out.println("task1 结束工作");
            return result;
        });
        CompletableFuture<Integer> task2 = task.thenApply((data) -> {
            System.out.println("走到正常的结束分支了,task1执行结果:" + data);
            return data * 2;
        });
        // task.exceptionally(...) 在原始任务(task)没有异常时,会直接透传原始结果,而不是执行异常处理逻辑。
        CompletableFuture<Integer> task3 = task.exceptionally((e) -> {
            System.out.println("走到异常分支了,异常信息为:" + e.getMessage());
            return -1;
        });
        // 如果出现异常并调用task2.get() 会直接抛出异常,如果不捕获则无法执行到task3.get()
        // 如果不调用task2.get() ,即使出现异常,也会走到task3.get(),并返回结果-1
        try {
            System.out.println("task2输出结果 " + task2.get());
        } catch (Exception e) {
            System.out.println("thenApply 链路异常: " + e.getMessage());
        }
        System.out.println("task3输出结果 " + task3.get());
    }

    // whenComplete whenComplete 会接收两个入参:
    // 入参 1 为上一个任务的返回值。
    // 入参 2 比较特殊,如果上一个任务抛出异常,则第 2 个入参不为空。
    // 需要注意的是 whenComplete 返回结果是上一个任务的执行结果,我们无法返回任务 2 的执行结果。
    public static void whenComplete() throws ExecutionException, InterruptedException {
        CompletableFuture<Integer> task1 = CompletableFuture.supplyAsync(() -> {
            System.out.println("task1 开始工作了");
            // 随机生成被除数,为0会抛出算术异常
            int num = RandomUtil.randomInt(0, 2);
            int result = 10 / num;
            System.out.println("task1 结束工作");
            return result;
        });
        CompletableFuture<Integer> task2 = task1.whenComplete((data, error) -> {
            System.out.println("任务2开始工作了");
            if (error != null) {
                try {
                    System.out.println("走到异常分支了,异常信息为:" + error.getMessage());
                } catch (Exception e) {
                    log.error("执行任务失败", e);
                }
                return;
            }
            System.out.println("走到正常的结束分支了,task1执行结果:" + data);
            System.out.println("任务2结束工作了");
        });
        System.out.println("输出结果为任务1的结果 " + task2.get());
    }

    // handle 使用和 whenComplete 差不多,唯一的区别就是 whenComplete 返回的是上一个任务的结果,而 handle 可以返回自己的结果。
    public static void handle() throws ExecutionException, InterruptedException {
        CompletableFuture<Integer> task1 = CompletableFuture.supplyAsync(() -> {
            System.out.println("task1 开始工作了");
            // 随机生成被除数,为0会抛出算术异常
            int num = RandomUtil.randomInt(0, 2);
            int result = 10 / num;
            System.out.println("task1 结束工作");
            return result;
        });
        CompletableFuture<Integer> task2 = task1.handle((data, error) -> {
            System.out.println("任务2开始工作了");
            if (error != null) {
                System.out.println("走到异常分支了,异常信息为:" + error.getMessage());
                return -1;
            }
            System.out.println("走到正常的结束分支了,task1执行结果:" + data);
            System.out.println("任务2结束工作了");
            return data + RandomUtil.randomInt(0, 10);
        });
        System.out.println("拿第2个任务的结果");
        System.out.println("第2个任务的结果 " + task2.get());
        System.out.println("第2个任务结果结束");
    }

    // thenCombine / thenAcceptBoth / runAfterBoth
    // 这几个方法都是将两个任务组合起来执行的,只有两个任务都顺利完成了,才会执行之后的方法,唯一的区别是:
    // thenCombine 接收两个任务的返回值,并返回自己的返回值。
    // thenAcceptBoth 接收两个参数返回值,但没有返回值。
    // runAfterBoth 既不能接收入参,也无返回值,待前两个任务执行完成后才能执行。

    public static void thenCombine() throws ExecutionException, InterruptedException {
        CompletableFuture<Integer> task1 = CompletableFuture.supplyAsync(() -> {
            System.out.println("task开始工作");
            int num = RandomUtil.randomInt(0, 100);
            System.out.println("task结束工作");
            return num;
        });
        CompletableFuture<Integer> task2 = CompletableFuture.supplyAsync(() -> {
            System.out.println("task2开始工作");
            int num = RandomUtil.randomInt(0, 100);
            System.out.println("task2结束工作");
            return num;
        });

        // 通过thenCombine将两个任务组合起来
        CompletableFuture<Integer> combineFuture = task1.thenCombine(task2, (result1, result2) -> {
            System.out.println("thenCombine执行了");
            System.out.println("task1返回结果:" + result1 + "  task2返回结果:" + result2);
            return result1 + result2;
        });
        System.out.println("thenCombine返回结果: " + combineFuture.get());

        // 通过 thenAcceptBoth 将两个任务组合起来,获取前两个任务处理结果,但自己不返回结果
        CompletableFuture<Void> acceptBothFuture = task1.thenAcceptBoth(task2, (result1, result2) -> {
            System.out.println("thenAcceptBoth执行了");
            System.out.println("task1返回结果:" + result1 + "  task2返回结果:" + result2);
        });
        System.out.println("thenAcceptBoth返回结果: " + acceptBothFuture.get());

        // 通过runAfterBoth将两个任务组合起来,不获取前两个任务处理结果,也不返回结果
        CompletableFuture<Void> runAfterBothFuture = task1.runAfterBoth(task2, () -> {
            System.out.println("runAfterBoth执行了");
        });
        System.out.println("runAfterBoth返回结果: " + runAfterBothFuture.get());
    }

    // applyToEither / acceptEither / runAfterEither
    // 这几个方法都是将两个任务组合起来执行的,只要有一个任务完成了,就可以执行之后的方法,唯一的区别是:
    // applyToEither 接收一个任务的返回值,并返回自己的返回值。
    // acceptEither 接收一个参数返回值,但没有返回值。
    // runAfterEither 既不能接收入参,也无返回值,待前两个任务执行完成后才能执行。
    public static void applyToEither() throws ExecutionException, InterruptedException {
        CompletableFuture<Integer> task2 = CompletableFuture.supplyAsync(() -> 2);
        CompletableFuture<Integer> task1 = CompletableFuture.supplyAsync(() -> 1);

        CompletableFuture<String> applyToEitherFuture = task1.applyToEither(task2, (result) -> {
            if (result == 1) {
                System.out.println("task1先完成任务");
                return "task1";
            }
            System.out.println("task2先完成任务");
            return "task2";
        });
        System.out.println("applyToEitherFuture 有一个任务完成了:" + applyToEitherFuture.get());

        CompletableFuture<Void> acceptEitherFuture = task1.acceptEither(task2, (result) -> {
            System.out.println("acceptEither result:" + result);
            if (result == 1) {
                System.out.println("task1先完成任务");
                return;
            }
            System.out.println("task2先完成任务");
        });
        System.out.println("acceptEitherFuture 有一个任务完成了");

        CompletableFuture<Void> runAfterEitherFuture = task1.runAfterEither(task2, () -> {
            System.out.println("有一个任务完成了");
        });
        System.out.println("runAfterEitherFuture 有一个任务完成了");
    }

    // thenCompose 方法会在某个任务执行完成后,将该任务的执行结果作为方法入参然后执行指定的方法,
    // 该方法会返回一个新的 CompletableFuture 实例,
    // 例如我们希望任务 1 执行完成后执行任务 2,任务 2 执行完成后返回执行任务 3,最终结果是从任务 3 中获取。

    public static void thenCompose() throws ExecutionException, InterruptedException {
        // 创建异步执行任务:
        CompletableFuture<Integer> task1 = CompletableFuture.supplyAsync(() -> {
            System.out.println("task1开始工作");
            int num = RandomUtil.randomInt(0, 5);
            try {
                TimeUnit.SECONDS.sleep(num);
            } catch (InterruptedException e) {
                log.error("task1异常", e);
            }
            System.out.println("task1结束工作,处理结果:" + num);
            return num;
        });
        CompletableFuture<String> task2 = task1.thenCompose((r) -> {
            System.out.println("task2 开始工作");
            int num = RandomUtil.randomInt(0, 5);
            try {
                TimeUnit.SECONDS.sleep(num);
            } catch (InterruptedException e) {
                log.error("task2异常", e);
            }
            System.out.println("task2 结束工作" + num);

            return CompletableFuture.supplyAsync(() -> {
                System.out.println("task3 开始工作,收到任务1的执行结果:" + r);
                return "task3 finished";
            });
        });
        System.out.println("执行结果->" + task2.get());
    }

    // allOf 返回的 CompletableFuture 是所有任务都执行完成后才会执行,只要有一个任务执行异常,则返回的 CompletableFuture 执行 get 方法时会抛出异常。
    // anyOf 则是只要有一个任务完成就可以触发后续方法,并且可以返回先完成任务的返回值,这一点和上述 applyToEither 例子差不多。
    public static void allOf() {
        CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
            // 模拟异步任务1
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                log.error("future1异常", e);
            }
            return "Hello";
        });

        CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
            // 模拟异步任务2
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                log.error("future2异常", e);
            }
            return "World";
        });

        CompletableFuture<Object> anyFuture = CompletableFuture.anyOf(future1, future2);
        // 任何一个异步任务完成后打印它的结果
        anyFuture.thenAccept(System.out::println);
        // 等待任何一个异步任务完成
        anyFuture.join();

        CompletableFuture<Void> allFutures = CompletableFuture.allOf(future1, future2);
        allFutures.thenRun(() -> {
            // 所有异步任务完成后打印它们的结果
            String result1 = future1.join();
            String result2 = future2.join();
            System.out.println(result1 + " " + result2);
        });
        // 等待所有异步任务完成
        allFutures.join();
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CompletableFutureTest.thenApply();
    }
}

回到目录

Mapper 热更新

@Slf4j
@Component
public class MapperHotSwap {

    @jakarta.annotation.Resource
    private MybatisProperties mybatisProperties;
    @jakarta.annotation.Resource
    private SqlSessionFactory sqlSessionFactory;

    private Resource[] mapperLocations;
    private Configuration config;
    // 记录文件内容是否变化
    private final HashMap<String, Long> fileChange = new HashMap<>();

    // WatchService 监听文件变化
    private WatchService watchService;

    @org.springframework.context.annotation.Configuration
    @ConfigurationProperties(prefix = MapperHotSwapProperties.PREFIX)
    @Data
    public static class MapperHotSwapProperties {
        public static final String PREFIX = "mybatis.mapper";
        private Boolean reload = false;
    }

    @jakarta.annotation.Resource
    private MapperHotSwapProperties hotSwapProperties;

    @PostConstruct
    public void init() {
        try {
            if (!hotSwapProperties.getReload()) {
                return;
            }
            prepareEnv();
            startWatcher(); // 启动 Watcher
            log.info("Mapper热更新已启动");
        } catch (Exception e) {
            log.error("Mapper热更新启动失败", e);
        }
    }

    /**
     * 初始化配置并注册 Watcher
     */
    public void prepareEnv() throws Exception {
        this.config = sqlSessionFactory.getConfiguration();
        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        this.mapperLocations = resolver.getResources(mybatisProperties.getMapperLocations()[0]);

        // 初始化 WatchService
        this.watchService = FileSystems.getDefault().newWatchService();

        Set<Path> watchedDirs = new HashSet<>();
        for (Resource mapperLocation : mapperLocations) {
            Path path = Paths.get(mapperLocation.getURI()).getParent();
            if (watchedDirs.add(path)) {
                // 只监听修改事件
                path.register(watchService, StandardWatchEventKinds.ENTRY_MODIFY);
            }
            String resourceName = mapperLocation.getFilename();
            long lastFrame = mapperLocation.contentLength() + mapperLocation.lastModified();
            fileChange.put(resourceName, lastFrame);
        }

        log.info("已注册目录监听,准备热更新");
    }

    /**
     * 启动 Watcher 线程
     */
    private void startWatcher() {
        Thread watcherThread = new Thread(() -> {
            try {
                WatchKey key;
                while ((key = watchService.take()) != null) {
                    for (WatchEvent<?> event : key.pollEvents()) {
                        Path changedFile = (Path) event.context();
                        onFileChange(changedFile);
                    }
                    key.reset();
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                log.error("文件监听中断", e);
            }catch (ClosedWatchServiceException e){
                log.info("文件监听服务关闭");
            }
        });
        watcherThread.setDaemon(true);
        watcherThread.start();
    }

    /**
     * 处理文件变更事件并触发热加载
     */
    private void onFileChange(Path changedFile) {
        String fileName = changedFile.getFileName().toString();
        for (Resource mapperLocation : mapperLocations) {
            String resourceName = mapperLocation.getFilename();
            if (resourceName != null && resourceName.equals(fileName)) {
                try {
                    long currentFrame = mapperLocation.contentLength() + mapperLocation.lastModified();
                    Long lastFrame = fileChange.get(resourceName);
                    boolean modifyFlag = lastFrame != null && currentFrame != lastFrame;
                    if (modifyFlag) {
                        fileChange.put(resourceName, currentFrame);
                        log.info("文件{}内容发生变化,开始热加载", resourceName);
                        reloadMapper();
                    }
                } catch (IOException e) {
                    log.error("读取文件失败: {}", fileName, e);
                }
                break;
            }
        }
    }

    /**
     * 执行 Mapper 文件热加载
     */
    private void reloadMapper() {
        try {
            removeConfig(config);
            for (Resource mapperLocation : mapperLocations) {
                try {
                    XMLMapperBuilder builder = new XMLMapperBuilder(
                            mapperLocation.getInputStream(), config, mapperLocation.toString(), config.getSqlFragments());
                    builder.parse();
                } catch (IOException e) {
                    log.error("mapper文件{}不存在或内部错误", mapperLocation.getFilename());
                }
            }
            log.info("Mapper文件已更新");
        } catch (Exception e) {
            log.error("Mapper文件更新失败:{}", e.getMessage());
        }
    }

    /**
     * 移除配置
     */
    private void removeConfig(Configuration configuration) {
        Class<?> classConfig = configuration.getClass();
        clearMap(classConfig, configuration, "mappedStatements");
        clearMap(classConfig, configuration, "caches");
        clearMap(classConfig, configuration, "resultMaps");
        clearMap(classConfig, configuration, "parameterMaps");
        clearMap(classConfig, configuration, "keyGenerators");
        clearMap(classConfig, configuration, "sqlFragments");
        clearSet(classConfig, configuration);
    }

    private void clearMap(Class<?> classConfig, Configuration configuration, String fieldName) {
        Field field = getDeclaredField(classConfig, fieldName);
        if (field == null) {
            return;
        }
        field.setAccessible(true);
        Map<?, ?> mapConfig = getFieldValue(field, configuration, Map.class);
        if (mapConfig != null) {
            mapConfig.clear();
        }
    }

    private void clearSet(Class<?> classConfig, Configuration configuration) {
        Field field = getDeclaredField(classConfig, "loadedResources");
        if (field == null) {
            return;
        }
        field.setAccessible(true);
        Set<?> setConfig = getFieldValue(field, configuration, Set.class);
        if (setConfig != null) {
            setConfig.clear();
        }
    }

    private <T> T getFieldValue(Field field, Object obj, Class<T> clazz) {
        try {
            Object value = field.get(obj);
            return clazz.isInstance(value) ? clazz.cast(value) : null;
        } catch (IllegalAccessException ignored) {
            return null;
        }
    }

    private Field getDeclaredField(Class<?> classConfig, String fieldName) {
        Field field = null;
        try {
            field = classConfig.getDeclaredField(fieldName);
        } catch (NoSuchFieldException ignored) {}
        return field;
    }

    @PreDestroy
    public void destroy() {
        try {
            if (watchService != null) {
                // 停止文件监听服务
                watchService.close();
            }
        } catch (IOException e) {
            log.warn("关闭 WatchService 失败", e);
        }
    }

}
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@ControllerAdvice
public class SessionExpiredRedirectAdvice implements ResponseBodyAdvice<Object> {

    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        // 只拦截特定的接口(这里假设是 /getUserInfo)
        return returnType.getExecutable().getName().contains("getUserInfo");
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType,
            MediaType selectedContentType, Class selectedConverterType,
            ServerHttpRequest request, ServerHttpResponse response) {

        if (body instanceof Map) {
            Map<?, ?> responseBody = (Map<?, ?>) body;
            if ("500".equals(String.valueOf(responseBody.get("code")))) {
                // 强制重定向到登录页
                HttpServletResponse servletResponse = ((ServletServerHttpResponse) response).getServletResponse();
                try {
                    servletResponse.sendRedirect("/login?expired=true");
                } catch (IOException e) {
                    throw new RuntimeException("Redirect failed", e);
                }
                return null; // 阻止原响应继续返回
            }
        }
        return body; // 其他情况正常返回
    }
}
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class SessionExpiredRedirectFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;

        // 包装 Response,以便读取响应内容
        ContentCachingResponseWrapper wrappedResponse = new ContentCachingResponseWrapper(httpResponse);

        // 继续执行后续逻辑(Controller)
        chain.doFilter(request, wrappedResponse);

        // 检查是否是需要处理的接口
        if (httpRequest.getRequestURI().endsWith("/getUserInfo")) {
            // 获取响应体
            byte[] responseBody = wrappedResponse.getContentAsByteArray();
            String responseStr = new String(responseBody);

            // 检查是否是 Session 过期(code=500)
            if (responseStr.contains("\"code\":500")) {
                // 强制重定向
                httpResponse.sendRedirect("/login?expired=true");
                return; // 终止后续处理
            }
        }

        // 正常情况:返回原始响应
        wrappedResponse.copyBodyToResponse();
    }
}

回到目录

达梦转金仓

import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.parsing.XNode;
import org.apache.ibatis.scripting.xmltags.XMLLanguageDriver;
import org.apache.ibatis.session.Configuration;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * @author zmy
 * @description 自定义语言驱动,将达梦中的 CFALGORITHMSDECRYPT 转换为 SM4
 * @note 首先设置金仓数据库的编码方式 {@code StringConverter.ENCODING = "UTF-8";},或者设置系统环境变量 KINGBASE_ENCODING = "UTF-8"
 * <p>1. springmvc 配置方式:
 * 在 mybatis.xml 中配置  {@code defaultScriptingLanguage} 属性为 {@link CustomLanguageDriver}
 * <pre>
 * {@code
 * <settings>
 *     <setting name="defaultScriptingLanguage" value="com.xxx.CustomLanguageDriver"/>
 * </settings>
 * }
 * </pre>
 *
 * <p>2. springboot 配置方式:在 {@link Configuration} 中配置 {@link CustomLanguageDriver}
 * <pre>
 * {@code
 * Configuration configuration = new Configuration();
 * configuration.setDefaultScriptingLanguage(CustomLanguageDriver.class);
 * // 注入到 SqlSessionFactoryBuilder 中
 * SqlSessionFactoryBuilder().build(configuration);
 * // 对于 SqlSessionFactoryBean 则是
 * sqlSessionFactoryBean.setConfiguration(config);
 * }
 * </pre>
 * @since 2025-07-04 20:35:33
 */
public class CustomLanguageDriver extends XMLLanguageDriver {
    private static final Pattern SCHEMA_PATTERN = Pattern.compile("\\bAUTHORITY\\.", Pattern.CASE_INSENSITIVE);
    private static final String NEW_SCHEMA = "DS_AUTHORITY.";

    @Override
    public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) {
        script = SCHEMA_PATTERN.matcher(script).replaceAll(NEW_SCHEMA);
        return super.createSqlSource(configuration, script, parameterType);
    }
    @Override
    public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
        SqlSource sqlSource = super.createSqlSource(configuration, script, parameterType);
        return new CustomSqlSource(configuration, sqlSource);
    }

    public static class CustomSqlSource implements SqlSource {
        private final SqlSource sqlSource;
        private final Configuration configuration;

        public CustomSqlSource(Configuration configuration, SqlSource sqlSource) {
            this.configuration = configuration;
            this.sqlSource = sqlSource;
        }
        @Override
        public BoundSql getBoundSql(Object parameterObject){
            BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
            String sql = boundSql.getSql();
            // 模式替换
            // sql = SCHEMA_PATTERN.matcher(sql).replaceAll(NEW_SCHEMA);
            // CFALGORITHMSDECRYPT替换为SM4
            sql = StringConverter.convertCryptName(sql);
            BoundSql newBoundSql = new BoundSql(configuration, sql, boundSql.getParameterMappings(), boundSql.getParameterObject());
            for (ParameterMapping mapping : boundSql.getParameterMappings()) {
                String prop = mapping.getProperty();
                if (boundSql.hasAdditionalParameter(prop)) {
                    newBoundSql.setAdditionalParameter(prop, boundSql.getAdditionalParameter(prop));
                }
            }
            return newBoundSql;
        }
    }

    public static class StringConverter {
        private static final String NEW_CRYPT = "SM4";
        private static final String CONVERT_FROM = "CONVERT_FROM";
        private static final String ENCODING = System.getenv("KINGBASE_ENCODING");
        private static final Pattern CRYPT_PATTERN = Pattern.compile(
                "\\bCFALGORITHMS(EN|DE)CRYPT\\s*\\(\\s*([?]|\\w+|'[^']+'|\"[^\"]+\")\\s*,\\s*\\d+\\s*,\\s*([?]|'[^']+'|\"[^\"]+\")\\s*\\)",
                Pattern.CASE_INSENSITIVE);
        private static final SqlCache sqlCache = new SqlCache();

        public static String convertCryptName(String sql) {
            String res = sqlCache.get(sql);
            if (res != null) {
                return res;
            }
            Matcher cryptMatcher = CRYPT_PATTERN.matcher(sql);
            StringBuilder sb = new StringBuilder();
            int lastEnd = 0;
            while (cryptMatcher.find()) {
                sb.append(sql, lastEnd, cryptMatcher.start());
                String encryptionType = cryptMatcher.group(1);
                String cryptData = cryptMatcher.group(2);
                String cryptKey = cryptMatcher.group(3);
                // 0表示加密,1表示解密
                if(encryptionType.equalsIgnoreCase("EN")){
                    // 加密替换为SM4(data, key, 0)
                    sb.append(NEW_CRYPT).append("(").append(cryptData).append(", ")
                            .append(cryptKey).append(", ").append(0).append(")");
                }else if(encryptionType.equalsIgnoreCase("DE")){
                    // 解密替换为CONVERT_FROM(SM4(data, key, 1), 'UTF-8')
                    sb.append(CONVERT_FROM).append("(").append(NEW_CRYPT).append("(").append(cryptData).append(", ")
                            .append(cryptKey).append(", ").append(1).append("), '").append(ENCODING).append("')");
                }else {
                    sb.append(sql, cryptMatcher.start(), cryptMatcher.end());
                }
                lastEnd = cryptMatcher.end();
            }
            sb.append(sql.substring(lastEnd));

            // 存入缓存
            res = sb.toString();
            sqlCache.put(sql, res);
            return res;
        }
        // 测试示例
        public static void main(String[] args) {
            String input1 = "update user_info set name =   CFALGORITHMSDECRYPT  (  ?  ,  514  , 'ssa' )," +
                    "age = ?, birthday = CFALGORITHMSENCRYPT (   ass             ,  123  ,  'abc'   )" +
                    "where id = ?";
            System.out.println(convertCryptName(input1));
        }
    }

    public static class SqlCache {
        // LRU淘汰的缓存容器
        private static final int MAX_CACHE_SIZE = 200;
        private final Map<String, String> cache;

        public SqlCache() {
            this.cache =  new ConcurrentHashMap<>(
                    new LinkedHashMap<>(MAX_CACHE_SIZE, 0.75f, true) {
                        @Override
                        protected boolean removeEldestEntry(Map.Entry<String, String> eldest) {
                            return size() > MAX_CACHE_SIZE; // 触发LRU淘汰
                        }
                    }
            );
        }

        // 存入缓存
        public void put(String sql, String result) {
            cache.put(sql, result);
        }

        // 获取缓存
        public String get(String sql) {
            return cache.get(sql);
        }

        // 清除缓存
        public void clear() {
            cache.clear();
        }
    }
}

回到目录

枚举替换if

// 调用支付的入口
@Component
@Slf4j
public class PayChannelStrategyService {
    Map<String, PayService> payServiceMap;
    // 注入所有的支付渠道
    PayChannelStrategyService(List<PayService> payServices){
        payServices.forEach(payService ->  payServiceMap.put(payService.getPayType(), payService));
    }

    public void pay(String payType) {
        PayService payService = payServiceMap.get(payType);
        if(payService == null){
            log.error("未找到对应的支付渠道");
            throw  new RuntimeException("未找到对应的支付渠道");
        }else{
            payService.pay();
        }
    }
}


// 支付渠道接口
public interface PayService {
    void pay();
    String getPayType();
}

// 具体的微信支付渠道
@Slf4j
@Service
public class WechatPayService implements PayService {
    @Override
    public void pay() {
        log.info("微信支付");
    }

    @Override
    public String getPayType() {
        return "wechat";
    }
}
// 支付宝支付
@Slf4j
@Service
public class ZhifubaoPayService implements PayService {
    @Override
    public void pay() {
        log.info("支付宝支付");
    }

    @Override
    public String getPayType() {
        return "zhifubao";
    }
}

// 有新的支付方式增加时,只需新增一个类
@Slf4j
@Service
public class BankPayService implements PayService {
    @Override
    public void pay() {
        log.info("使用银行卡支付");
    }

    @Override
    public String getPayType() {
        return "bank";
    }
}

回到目录

获取字典json

@Getter
@AllArgsConstructor
public enum Gender implements IDictionary {
    MALE(1, "男", "#409EFF", "#409EFF"),
    FEMALE(2, "女", "#67C23A", "#67C23A");

    private final int key;
    private final String label;
    private final String color;
    private final String backgroundColor;
}

@FunctionalInterface
public interface IFunction<T, R> extends Function<T, R>, Serializable {
}

@Slf4j
public class DictionaryUtil {
    public static <D extends IDictionary> @NotNull List<Map<String, Object>> getDictionaryList(@NotNull Class<D> clazz) {
        return getDictionaryLists(clazz, "key", "label");
    }

    public static <D extends IDictionary> @NotNull List<Map<String, Object>> getDictionaryLists(@NotNull Class<D> clazz, String... props) {
        List<Map<String, Object>> mapList = new ArrayList<>();
        //取出所有枚举类型
        Arrays.stream(clazz.getEnumConstants()).forEach(enumItem -> {
            Map<String, Object> item = new HashMap<>(2);
            for (String prop : props) {
                try {
                    Method method = clazz.getMethod("get" + StringUtils.capitalize(prop));
                    Object value = method.invoke(enumItem);
                    item.put(prop, value);
                } catch (Exception exception) {
                    log.error(exception.getMessage(), exception);
                }
            }
            mapList.add(item);
        });
        return mapList;
    }

    // 这里Gender::getKey 是(Gender enumItem)-> enumItem.getKey(),实际是Function<Gender, String>
    public static <D extends IDictionary> @NotNull List<Map<String, Object>> getDictionaryLists(@NotNull Class<D> clazz, IFunction<D, ?>... lambdas) {
        List<Map<String, Object>> mapList = new ArrayList<>();
        Arrays.stream(clazz.getEnumConstants()).forEach(enumItem -> {
            Map<String, Object> item = new HashMap<>(lambdas.length);
            // 依次取出参数的值
            Arrays.stream(lambdas).forEach(lambda -> {
                try {
                    String prop = StringUtils.uncapitalize(getLambdaFunctionName(lambda));
                    item.put(prop, lambda.apply(enumItem));
                    Supplier<Object> supplier = () -> lambda.apply(enumItem);
                } catch (Exception exception) {
                    log.error(exception.getMessage(), exception);
                }
            });
            mapList.add(item);
        });
        return mapList;
    }

    public static @NotNull String getLambdaFunctionName(@NotNull IFunction<?, ?> lambda) {
        try {
            // writeReplace是序列化规范中定义的特殊方法
            // 在对象序列化时,如果类A定义了此方法返回B,序列化机制会调用它来获取一个替代对象b进行序列化
            // 反序列化时会调用B中的 readResolve 方法 - 反序列化时还原为原始对象b
            // 对于 Lambda 表达式,Java 编译器会生成一个包含此方法的类,用于支持 Lambda 的序列化
            Method replaceMethod = lambda.getClass().getDeclaredMethod("writeReplace");
            replaceMethod.setAccessible(true);
            SerializedLambda serializedLambda = (SerializedLambda) replaceMethod.invoke(lambda);
            return serializedLambda.getImplMethodName().replace("get", "");
        } catch (Exception exception) {
            throw new RuntimeException(exception);
        }
    }
    public static void main(String[] args) {
        System.out.println(DictionaryUtil.getDictionaryLists(Gender.class, Gender::getLabel, Gender::getColor, Gender::getKey, Gender::getBackgroundColor));
    }
}