HSSFWorkbook 用于 .xls,XSSFWorkbook 用于 .xlsx。SharedStringsTable(SST),其他类型(数字、布尔、日期等)的值直接写在 <v>xxx</v> 中,不会出现在 SST 里。OPCPackage 可以构造 WorkBook:XSSFWorkbook workbook = new XSSFWorkbook(pkg)[xl]
└── worksheets
└── sheet1.xml ← 这就是数据所在的地方!
└── style.xml ← 这就是样式所在的地方!
└── sharedStrings.xml ← 所有字符串集中存放在这里(节省空间)
[Content_Types].xml
背景:OPC 包结构与 Relationship
.xlsx 文件本质上是一个 ZIP 包,遵循 Open Packaging Conventions (OPC) 标准。在这个标准中:workbook、worksheet、shared strings)都是一个 Part(部件)Part 之间通过 Relationships(关系) 相互引用,每个关系都有:rId1, rId2)Type),用 URI 表示Target),指向另一个 Part 的路径SAX 解析(事件驱动)。它的工作流程是:从 ZIP 里拿出 sheet1.xml 的输入流;逐行、逐字符地“扫描”这个 XML;
每当遇到一个 xml 元素的开头(<row,<c 等)就调用 startElement(String uri, String localName, String name, Attributes attributes) 方法,attributes 可以拿到元素的属性的值,attributes.getValue("propName")
在 SAX 解析器遇到 XML 元素中的「文本内容」时被调用 characters(char[] ch, int start, int length),把其中的内容 xxx 读取出来,characters() 可能被多次调用,SAX 不保证整个文本内容一次性传完!
遇到 xml 元素的结尾(</row>,</c> 等)就调用 endElement(String uri, String localName, String name) 处理读取到的数据,如果是共享字符串,从 sst.getItemAt(idx).getString() 获取
三个步骤构成对一个元素的处理,其中用到一些字段存储中间结果
sheet1.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&"@@"&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 的值和含义如下:| 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 通常不写这个(默认就是数字) |
styles.xml 解析(复杂,POI 不直接支持,这非常复杂,POI 官方示例 XLSX2CSV 实现了这个逻辑。);s="x",style.xml 中的 cellXfs 元素是样式,x 是样式索引,cellXfs 的子元素 xf 的 numFmtId 属性表示样式类型,其中 ID 为 14~22 是内置日期格式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;
}
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> 的内容)时调用
public void characters(char[] ch, int start, int length) {
if (insideV) {
currentValue.append(ch, start, length); // ← 只收 <v> 的内容
}
// 忽略 <f>、<is> 等其他标签的文本
}
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 用默认
}
}
}
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
*/
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(),无需手动调用
}
}
| 中文字号 | 磅值 (pt) | OOXML 底层 w:val (半磅) |
|---|---|---|
| 初号 | 42pt | 84 |
| 小初 | 36pt | 72 |
| 一号 | 26pt | 52 |
| 小一 | 24pt | 48 |
| 二号 | 22pt | 44 |
| 小二 | 18pt | 36 |
| 三号 | 16pt | 32 |
| 小三 | 15pt | 30 |
| 四号 | 14pt | 28 |
| 小四 | 12pt | 24 |
| 五号 | 10.5pt | 21 |
| 小五 | 9pt | 18 |
| 六号 | 7.5pt | 15 |
| 小六 | 6.5pt | 13 |
| 七号 | 5.5pt | 11 |
| 八号 | 5pt | 10 |
XWPFDocument:表示整个 Word 文档对象,文档的入口点,包含所有段落、表格、页眉页脚等,主要用于处理 docx,HWPFDocument 用于处理 docXWPFDocument 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 是 Apache POI 提供的一个专门用于快速提取 .docx 文档内容的工具类。它的主要特点是占用内存少、速度快,但功能有限,主要用于文本提取而非文档编辑。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:表示文档中的一个段落,段落是文档的基本文本单元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:表示段落中的一段文本运行,同一段落中可以有不同的运行,每个运行可以有不同的格式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:表示文档中的表格,创建和操作表格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)); // 设置宽度
XWPFTableRow:表示表格行, 包含单元格的容器XWPFTableCell:表示表格单元格, 可以包含段落、表格等cell.setText("单元格内容");
cell.setColor("FFFFFF"); // 设置背景色
cell.setVerticalAlignment(XWPFTableCell.XWPFVertAlign.CENTER); // 垂直对齐
// 在单元格中添加段落
XWPFParagraph cellPara = cell.addParagraph();
XWPFRun cellRun = cellPara.createRun();
cellRun.setText("单元格中的文本");
XWPFStyles:文档的样式集合, 管理文档中的所有样式XWPFStyle:表示单个样式, 定义段落或文本的格式XWPFStyles styles = doc.getStyles();
String styleId = para.getStyleID();
String styleId = run.getStyle();
XWPFStyle style = styles.getStyle(styleId); // 根据样式ID获取样式
XWPFStyle newStyle = styles.newStyle("MyStyle"); // 创建新样式
XWPFHeader:文档页眉XWPFFooter:文档页脚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:文档的编号定义, 管理编号和项目符号列表XWPFNumbering numbering = doc.getNumbering();
CTAbstractNum abstractNum = CTAbstractNum.Factory.newInstance();
XWPFHyperlink:表示超链接, 创建文档中的链接// 创建超链接
CTHyperlink cthyper = paragraph.getCTP().addNewHyperlink();
cthyper.setId(hyperlink.getId());
XWPFHyperlink link = new XWPFHyperlink(cthyper);
CTP(org.openxmlformats.schemas.wordprocessingml.x2006.main.CTP):XML级别的段落表示, 作用:低级别操作段落XMLCTR(org.openxmlformats.schemas.wordprocessingml.x2006.main.CTR):XML级别的运行表示, 作用:低级别操作文本运行XMLCTPr:段落属性, 作用:XML级别的段落格式设置CTRPr:运行属性, 作用:XML级别的文本格式设置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);
}
convertToPdf,1.1.0 不存在
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>() {
public void invoke(EventData data, AnalysisContext context) {
log.info("读取到一条数据: {}", JSONUtil.toJsonStr(data));
}
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> {
public void invoke(EventData data, AnalysisContext context) {
log.info("解析到一条数据:{}", JSONUtil.toJsonStr(data));
}
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);
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();
}
}
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> {
public Class<?> supportJavaTypeKey() {
// 支持的 Java 类型
return EventTypeEnum.class;
}
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()));
}*/
public EventTypeEnum convertToJavaData(ReadConverterContext<?> context) {
return EventTypeEnum.codeOf(Integer.valueOf(context.getReadCellData().getStringValue()));
}
// 写到excel中的都转为String,写为WriteCellData<String>
public WriteCellData<String> convertToExcelData(WriteConverterContext<EventTypeEnum> context) {
return new WriteCellData<>(String.valueOf(context.getValue().getCode()));
}
}
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();
}
}
public class MapperHotSwap {
.annotation.Resource
private MybatisProperties mybatisProperties;
.annotation.Resource
private SqlSessionFactory sqlSessionFactory;
private Resource[] mapperLocations;
private Configuration config;
// 记录文件内容是否变化
private final HashMap<String, Long> fileChange = new HashMap<>();
// WatchService 监听文件变化
private WatchService watchService;
.springframework.context.annotation.Configuration
(prefix = MapperHotSwapProperties.PREFIX)
public static class MapperHotSwapProperties {
public static final String PREFIX = "mybatis.mapper";
private Boolean reload = false;
}
.annotation.Resource
private MapperHotSwapProperties hotSwapProperties;
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;
}
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;
public class SessionExpiredRedirectAdvice implements ResponseBodyAdvice<Object> {
public boolean supports(MethodParameter returnType, Class converterType) {
// 只拦截特定的接口(这里假设是 /getUserInfo)
return returnType.getExecutable().getName().contains("getUserInfo");
}
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;
public class SessionExpiredRedirectFilter implements Filter {
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.";
public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) {
script = SCHEMA_PATTERN.matcher(script).replaceAll(NEW_SCHEMA);
return super.createSqlSource(configuration, script, parameterType);
}
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;
}
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) {
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();
}
}
}
// 调用支付的入口
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();
}
// 具体的微信支付渠道
public class WechatPayService implements PayService {
public void pay() {
log.info("微信支付");
}
public String getPayType() {
return "wechat";
}
}
// 支付宝支付
public class ZhifubaoPayService implements PayService {
public void pay() {
log.info("支付宝支付");
}
public String getPayType() {
return "zhifubao";
}
}
// 有新的支付方式增加时,只需新增一个类
public class BankPayService implements PayService {
public void pay() {
log.info("使用银行卡支付");
}
public String getPayType() {
return "bank";
}
}
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;
}
public interface IFunction<T, R> extends Function<T, R>, Serializable {
}
public class DictionaryUtil {
public static <D extends IDictionary> List<Map<String, Object>> getDictionaryList( Class<D> clazz) {
return getDictionaryLists(clazz, "key", "label");
}
public static <D extends IDictionary> List<Map<String, Object>> getDictionaryLists( 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> List<Map<String, Object>> getDictionaryLists( 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 String getLambdaFunctionName( 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));
}
}