--------------------------------------------------------------------------------------------------------
这段代码用于通过自定义的
ExportExcel 工具类生成 Excel 文件并写入输出流,主要功能是导出案件信息数据。以下是代码的详细分析,包括逻辑流程、潜在问题及优化建议:一、代码逻辑流程
-
初始化导出工具
ExportExcel excel = new ExportExcel(lamCaseReportVOS.length);- 创建
ExportExcel实例,参数为数据数组lamCaseReportVOS的长度(可能用于预分配资源或设置数据量)。
- 创建
-
设置注解配置与创建工作表
excel.setAnnotationList(annotationListResult);:设置注解列表(推测用于 Excel 表头、字段映射等配置,如自定义列名、格式等)。excel.addSheetAddSortNum("案件信息", LamCaseReportExportVO.class);:添加名为 “案件信息” 的工作表,并通过LamCaseReportExportVO类的结构生成表头(可能包含序号列,因方法名含AddSortNum)。
-
工作表配置
Sheet sheet = excel.getSheet();:获取当前工作表对象。sheet.createFreezePane(0, 1);:冻结首行(参数0,1表示冻结第 0 列前的列、第 1 行前的行,即首行固定),方便滚动时查看表头。
-
填充数据与写入输出流
excel.setDataList(Arrays.asList(lamCaseReportVOS), true);:将数组转换为列表并设置到 Excel 中(第二个参数true可能表示启用某种数据处理,如格式转换)。wb = excel.getWb();:获取 Workbook 对象(POI 库的核心类,代表整个 Excel 文件)。wb.write(outputStream);:将 Excel 内容写入输出流(可能是网络响应流或文件流)。
-
资源释放与日志记录
excel.dispose();:释放ExportExcel内部资源。outputStream.flush();:刷新输出流,确保数据全部写出。wb.close();:关闭 Workbook,释放 POI 占用的资源(如文件句柄)。- 记录导出总耗时(通过
stopWatch)。
二、潜在问题与风险
-
输出流关闭问题
- 代码中仅调用
outputStream.flush(),但未显式关闭outputStream。若outputStream是网络响应流(如HttpServletResponse.getOutputStream()),可能导致连接泄露;若为文件流,可能导致文件损坏或占用磁盘资源。 - 建议:在
finally中关闭输出流(需判断非空):java运行if (outputStream != null) {try {outputStream.close();} catch (IOException e) {log.error("输出流关闭失败", e);} }
- 代码中仅调用
-
异常处理缺失
- 核心逻辑(如
wb.write(outputStream))可能抛出IOException,但代码未捕获,可能导致异常向上传播时未被处理,影响调用方(如 Web 场景下返回 500 错误但无详细日志)。 - 建议:在
try块中捕获异常并记录,或在方法上声明throws IOException由上层处理,避免静默失败。
- 核心逻辑(如
-
Workbook 关闭顺序
- 代码中先
wb.close()再处理其他资源,但Workbook.close()可能内部已包含流的关闭(如 POI 的XSSFWorkbook),若outputStream依赖 Workbook 状态,可能导致数据未完全写出。 - 建议:确保
wb.write(outputStream)完成后,先刷新流,再关闭 Workbook,最后关闭输出流(顺序:write→flush→wb.close()→outputStream.close())。
- 代码中先
-
大数量导出的内存风险
- 若
lamCaseReportVOS数据量过大(如十万级以上),Arrays.asList(lamCaseReportVOS)会将所有数据加载到内存,结合 POI 的普通写入模式(非 SXSSF),可能导致 OOM(内存溢出)。 - 判断依据:若
ExportExcel内部使用HSSFWorkbook(.xls)或普通XSSFWorkbook(.xlsx),则存在此风险;若使用SXSSFWorkbook(POI 的低内存模式),则风险较低。 - 建议:确认
ExportExcel是否支持大数据量导出,若不支持,需分批次写入或切换为SXSSFWorkbook。
- 若
-
变量
legCaseReport = null无实际意义- 局部变量在方法结束后会自动回收,手动赋值
null对垃圾回收无明显作用,可删除。
- 局部变量在方法结束后会自动回收,手动赋值
三、优化建议
-
完善资源关闭逻辑确保所有可关闭资源(
Workbook、OutputStream)在finally中正确关闭,避免泄露。 -
添加异常日志在
try-catch中记录关键步骤的异常(如写入失败、资源关闭失败),方便排查问题:java运行try {// 核心逻辑 } catch (IOException e) {log.error("Excel导出失败", e);throw new RuntimeException("导出失败", e); // 根据业务需要决定是否抛出 } finally {// 资源释放 } -
验证数据非空若
lamCaseReportVOS为null,lamCaseReportVOS.length会抛NullPointerException,建议先判断:java运行if (lamCaseReportVOS == null) {lamCaseReportVOS = new LamCaseReportExportVO[0]; // 空数组处理 } ExportExcel excel = new ExportExcel(lamCaseReportVOS.length); -
确认 Excel 格式与版本根据
Workbook类型判断导出的是.xls(HSSF,最大 65536 行)还是.xlsx(XSSF/SXSSF,支持百万级行),避免数据量超过格式限制导致失败。 -
监控大文件导出性能若导出文件较大,
stopWatch记录的总耗时可用于性能分析,若耗时过长,可考虑异步导出(返回下载链接,后台异步生成文件)。
四、总结
这段代码实现了 Excel 导出的核心逻辑,但在资源管理和异常处理上存在优化空间。主要需关注:
- 确保
OutputStream和Workbook正确关闭; - 处理可能的异常并记录日志;
- 评估数据量,避免大文件导出导致的内存问题。
优化后可提高代码的健壮性和可维护性。
--------------------------------------------------------------------------------------------------------
这段代码的主要功能是根据输入的列名字符串(
columns),从指定类(LamCaseReportExportVO)的 Excel 注解配置中筛选并排序对应的列信息,最终返回筛选后的列配置列表。常用于 Excel 导出时,根据前端指定的列名动态调整导出的列及其顺序。代码逐行解析
1. 方法定义与参数校验
java
运行
private List<Object[]> getColumnsNew(String columns) throws NoSuchFieldException, IllegalAccessException {if (StringUtils.isBlank(columns)) {throw new BusinessException("导出列数据不能为空");}// ... 后续逻辑
}
- 功能:定义一个私有方法,接收字符串参数
columns(格式如"name,age,gender"),返回List<Object[]>(存储筛选后的列配置)。 - 参数校验:若
columns为空或空白,抛出业务异常(防止后续处理出错)。
2. 获取原始注解配置列表
java
运行
List<Object[]> annotationList = ExcelUtil.getAnnotationList(LamCaseReportExportVO.class);
- 功能:通过工具类
ExcelUtil获取LamCaseReportExportVO类中所有带 Excel 导出注解(如@ExcelField)的字段配置。 - 返回值说明:
annotationList是一个列表,每个元素Object[]存储单个字段的注解信息,推测结构为:objects[0]:注解对象(如ExcelField实例,包含列名、排序、格式等配置)。objects[1]:字段对应的 “标识值”(可能是字段名或注解中定义的唯一标识,用于匹配columns参数)。
3. 初始化结果列表与解析输入列
java
运行
List<Object[]> annotationListResult = new ArrayList<>();
if (StringUtils.isNotEmpty(columns)) {String[] split = columns.split(","); // 按逗号分割输入的列名,得到需要导出的列数组// ... 循环处理每一列
}
- 功能:初始化结果列表
annotationListResult,并将输入的columns按逗号分割为数组split(如"name,age"分割为["name", "age"])。
4. 筛选并调整列顺序
java
运行
for (int i = 0; i < split.length; i++) { // 遍历输入的每一列(i为目标顺序索引)for (Object[] objects : annotationList) { // 遍历原始注解配置中的每一列// 校验原始注解配置的有效性(确保有注解对象和标识值)if (objects.length > 1 && objects[0] != null && objects[1] != null) {ExcelField excelField = ((ExcelField) objects[0]); // 获取注解对象String value = objects[1].toString(); // 获取字段标识值(用于匹配)// 若输入的列标识与原始配置中的标识匹配if (split[i].equals(value)) {// 以下代码用于动态修改注解的sort属性(调整列的显示顺序)InvocationHandler invocationHandler = Proxy.getInvocationHandler(excelField);Field memberValues = invocationHandler.getClass().getDeclaredField("memberValues");memberValues.setAccessible(true); // 突破私有访问限制Map<String, Integer> memberValuesMap = (Map) memberValues.get(invocationHandler);memberValuesMap.put("sort", i); // 将当前列的排序值设为循环索引i(保证顺序与输入一致)annotationListResult.add(objects); // 将匹配的列配置加入结果列表}}}
}
- 核心逻辑:
- 外层循环遍历输入的列(
split数组),i表示该列在最终导出结果中的顺序(从 0 开始)。 - 内层循环遍历原始注解配置(
annotationList),寻找与当前输入列(split[i])匹配的字段。 - 匹配条件:原始配置中的字段标识值(
objects[1])与输入列名(split[i])相等。 - 动态调整排序:通过反射修改
ExcelField注解的sort属性(排序值)为i,确保导出时列的顺序与输入columns的顺序一致。 - 将匹配的列配置加入结果列表
annotationListResult。
- 外层循环遍历输入的列(
5. 返回结果
java
运行
return annotationListResult;
- 返回筛选并排序后的列配置列表,供后续 Excel 导出时使用(如生成表头、填充数据)。
关键技术点:动态修改注解属性
注解在 Java 中默认是 “不可变的”,但代码中通过反射操作代理对象实现了动态修改
ExcelField的sort属性:Proxy.getInvocationHandler(excelField):获取注解对象的代理处理器(注解本质是通过动态代理实现的)。memberValues字段:代理处理器中存储注解属性键值对的 Map(私有字段,需通过setAccessible(true)突破访问限制)。memberValuesMap.put("sort", i):修改sort属性的值为当前列的顺序索引i,确保导出时按输入顺序排列。
功能总结
- 作用:根据输入的列名字符串(
columns),从LamCaseReportExportVO类的 Excel 注解中筛选出需要导出的列,并按输入顺序调整列的排序。 - 应用场景:支持 Excel 导出时的 “动态列选择”,用户可指定需要导出的列及其显示顺序,增强导出功能的灵活性。
- 注意点:依赖
ExcelUtil.getAnnotationList方法正确解析类中的 Excel 注解,且columns参数的列名需与注解中的标识值一致(否则会筛选不到对应列)。
--------------------------------------------------------------------------------------------------------
问题的核心在于:多个线程并发调用时,通过反射修改了
ExcelField注解的sort属性,导致LamCaseReportExportVO.class中注解的原始sort值被污染(因为注解实例在 JVM 中是单例的)。解决思路是:避免直接修改原始注解对象的属性,而是为每个线程创建注解的副本(或新实例),在副本上修改
sort值,确保原始注解的初始属性不受影响。具体修改方案
步骤 1:新增 “复制注解” 的工具方法
通过动态代理创建
ExcelField注解的副本,保留原始属性并允许修改,避免污染原始注解。java
运行
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;/*** 复制注解的工具类,避免修改原始注解*/
public class AnnotationCopyUtil {/*** 复制ExcelField注解,返回新的代理实例*/public static ExcelField copyExcelField(ExcelField original) {if (original == null) {return null;}// 获取原始注解的属性值Map<String, Object> originalValues = getAnnotationValues(original);// 创建新的代理实例,代理ExcelField接口return (ExcelField) Proxy.newProxyInstance(ExcelField.class.getClassLoader(),new Class[]{ExcelField.class},new AnnotationInvocationHandler(ExcelField.class, originalValues));}/*** 获取注解的所有属性值(通过反射)*/private static Map<String, Object> getAnnotationValues(Annotation annotation) {try {InvocationHandler handler = Proxy.getInvocationHandler(annotation);java.lang.reflect.Field memberValuesField = handler.getClass().getDeclaredField("memberValues");memberValuesField.setAccessible(true);return new HashMap<>((Map<String, Object>) memberValuesField.get(handler));} catch (Exception e) {throw new RuntimeException("获取注解属性失败", e);}}/*** 自定义代理处理器,用于处理新注解的方法调用*/private static class AnnotationInvocationHandler implements InvocationHandler {private final Class<? extends Annotation> annotationType;private final Map<String, Object> values;public AnnotationInvocationHandler(Class<? extends Annotation