ExportExcel-AnnotationList-反射

--------------------------------------------------------------------------------------------------------

这段代码用于通过自定义的 ExportExcel 工具类生成 Excel 文件并写入输出流,主要功能是导出案件信息数据。以下是代码的详细分析,包括逻辑流程、潜在问题及优化建议:

一、代码逻辑流程

  1. 初始化导出工具ExportExcel excel = new ExportExcel(lamCaseReportVOS.length);
    • 创建 ExportExcel 实例,参数为数据数组 lamCaseReportVOS 的长度(可能用于预分配资源或设置数据量)。
  2. 设置注解配置与创建工作表
    • excel.setAnnotationList(annotationListResult);:设置注解列表(推测用于 Excel 表头、字段映射等配置,如自定义列名、格式等)。
    • excel.addSheetAddSortNum("案件信息", LamCaseReportExportVO.class);:添加名为 “案件信息” 的工作表,并通过 LamCaseReportExportVO 类的结构生成表头(可能包含序号列,因方法名含 AddSortNum)。
  3. 工作表配置
    • Sheet sheet = excel.getSheet();:获取当前工作表对象。
    • sheet.createFreezePane(0, 1);:冻结首行(参数 0,1 表示冻结第 0 列前的列、第 1 行前的行,即首行固定),方便滚动时查看表头。
  4. 填充数据与写入输出流
    • excel.setDataList(Arrays.asList(lamCaseReportVOS), true);:将数组转换为列表并设置到 Excel 中(第二个参数 true 可能表示启用某种数据处理,如格式转换)。
    • wb = excel.getWb();:获取 Workbook 对象(POI 库的核心类,代表整个 Excel 文件)。
    • wb.write(outputStream);:将 Excel 内容写入输出流(可能是网络响应流或文件流)。
  5. 资源释放与日志记录
    • excel.dispose();:释放 ExportExcel 内部资源。
    • outputStream.flush();:刷新输出流,确保数据全部写出。
    • wb.close();:关闭 Workbook,释放 POI 占用的资源(如文件句柄)。
    • 记录导出总耗时(通过 stopWatch)。

二、潜在问题与风险

  1. 输出流关闭问题
    • 代码中仅调用 outputStream.flush(),但未显式关闭 outputStream。若 outputStream 是网络响应流(如 HttpServletResponse.getOutputStream()),可能导致连接泄露;若为文件流,可能导致文件损坏或占用磁盘资源。
    • 建议:在 finally 中关闭输出流(需判断非空):
      java
       
      运行
       
       
       
       
      if (outputStream != null) {
          try {
              outputStream.close();
          } catch (IOException e) {
              log.error("输出流关闭失败", e);
          }
      }
      
       
       
  2. 异常处理缺失
    • 核心逻辑(如 wb.write(outputStream))可能抛出 IOException,但代码未捕获,可能导致异常向上传播时未被处理,影响调用方(如 Web 场景下返回 500 错误但无详细日志)。
    • 建议:在 try 块中捕获异常并记录,或在方法上声明 throws IOException 由上层处理,避免静默失败。
  3. Workbook 关闭顺序
    • 代码中先 wb.close() 再处理其他资源,但 Workbook.close() 可能内部已包含流的关闭(如 POI 的 XSSFWorkbook),若 outputStream 依赖 Workbook 状态,可能导致数据未完全写出。
    • 建议:确保 wb.write(outputStream) 完成后,先刷新流,再关闭 Workbook,最后关闭输出流(顺序:write → flush → wb.close() → outputStream.close())。
  4. 大数量导出的内存风险
    • 若 lamCaseReportVOS 数据量过大(如十万级以上),Arrays.asList(lamCaseReportVOS) 会将所有数据加载到内存,结合 POI 的普通写入模式(非 SXSSF),可能导致 OOM(内存溢出)。
    • 判断依据:若 ExportExcel 内部使用 HSSFWorkbook.xls)或普通 XSSFWorkbook.xlsx),则存在此风险;若使用 SXSSFWorkbook(POI 的低内存模式),则风险较低。
    • 建议:确认 ExportExcel 是否支持大数据量导出,若不支持,需分批次写入或切换为 SXSSFWorkbook
  5. 变量 legCaseReport = null 无实际意义
    • 局部变量在方法结束后会自动回收,手动赋值 null 对垃圾回收无明显作用,可删除。

三、优化建议

  1. 完善资源关闭逻辑确保所有可关闭资源(WorkbookOutputStream)在 finally 中正确关闭,避免泄露。
  2. 添加异常日志在 try-catch 中记录关键步骤的异常(如写入失败、资源关闭失败),方便排查问题:
    java
     
    运行
     
     
     
     
    try {
        // 核心逻辑
    } catch (IOException e) {
        log.error("Excel导出失败", e);
        throw new RuntimeException("导出失败", e); // 根据业务需要决定是否抛出
    } finally {
        // 资源释放
    }
    
     
     
  3. 验证数据非空若 lamCaseReportVOS 为 nulllamCaseReportVOS.length 会抛 NullPointerException,建议先判断:
    java
     
    运行
     
     
     
     
    if (lamCaseReportVOS == null) {
        lamCaseReportVOS = new LamCaseReportExportVO[0]; // 空数组处理
    }
    ExportExcel excel = new ExportExcel(lamCaseReportVOS.length);
    
     
     
  4. 确认 Excel 格式与版本根据 Workbook 类型判断导出的是 .xls(HSSF,最大 65536 行)还是 .xlsx(XSSF/SXSSF,支持百万级行),避免数据量超过格式限制导致失败。
  5. 监控大文件导出性能若导出文件较大,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); // 将匹配的列配置加入结果列表
            }
        }
    }
}
 
  • 核心逻辑:
    1. 外层循环遍历输入的列(split数组),i表示该列在最终导出结果中的顺序(从 0 开始)。
    2. 内层循环遍历原始注解配置(annotationList),寻找与当前输入列(split[i])匹配的字段。
    3. 匹配条件:原始配置中的字段标识值(objects[1])与输入列名(split[i])相等。
    4. 动态调整排序:通过反射修改ExcelField注解的sort属性(排序值)为i,确保导出时列的顺序与输入columns的顺序一致。
    5. 将匹配的列配置加入结果列表annotationListResult

5. 返回结果

java
 
运行
 
 
 
 
return annotationListResult;
 
  • 返回筛选并排序后的列配置列表,供后续 Excel 导出时使用(如生成表头、填充数据)。

关键技术点:动态修改注解属性

注解在 Java 中默认是 “不可变的”,但代码中通过反射操作代理对象实现了动态修改ExcelFieldsort属性:
  1. Proxy.getInvocationHandler(excelField):获取注解对象的代理处理器(注解本质是通过动态代理实现的)。
  2. memberValues字段:代理处理器中存储注解属性键值对的 Map(私有字段,需通过setAccessible(true)突破访问限制)。
  3. 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> annotationType, Map<String, Object> values) {
            this.annotationType = annotationType;
            this.values = new HashMap<>(values); // 用新的Map存储属性,避免修改原始数据
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            String methodName = method.getName();
            if (methodName.equals("annotationType")) {
                return annotationType;
            }
            if (methodName.equals("toString")) {
                return annotationType.getName() + values.toString();
            }
            if (methodName.equals("hashCode")) {
                return calculateHashCode();
            }
            if (methodName.equals("equals")) {
                return proxy == args[0];
            }
            // 其他方法(属性getter)返回对应的值
            return values.get(methodName);
        }

        private int calculateHashCode() {
            int hashCode = 0;
            for (Map.Entry<String, Object> entry : values.entrySet()) {
                hashCode += 127 * entry.getKey().hashCode() ^ entry.getValue().hashCode();
            }
            return hashCode;
        }
    }
}
 

步骤 2:修改原方法,使用副本注解修改sort

核心改动:对从annotationList中获取的ExcelField注解,先复制一个副本,再修改副本的sort值,避免影响原始注解。
java
 
运行
 
 
 
 
private List<Object[]> getColumnsNew(String columns) throws NoSuchFieldException, IllegalAccessException {
    if (StringUtils.isBlank(columns)) {
        throw new BusinessException("导出列数据不能为空");
    }
    // 1. 获取原始注解列表(此时sort为初始值)
    List<Object[]> annotationList = ExcelUtil.getAnnotationList(LamCaseReportExportVO.class);
    List<Object[]> annotationListResult = new ArrayList<>();
    
    if (StringUtils.isNotEmpty(columns)) {
        String[] split = columns.split(",");
        for (int i = 0; i < split.length; i++) {
            for (Object[] objects : annotationList) {
                if (objects.length > 1 && objects[0] != null && objects[1] != null) {
                    // 2. 关键:复制原始注解,得到一个新的副本(避免修改原始注解)
                    ExcelField originalExcelField = (ExcelField) objects[0];
                    ExcelField copiedExcelField = AnnotationCopyUtil.copyExcelField(originalExcelField);
                    
                    String value = objects[1].toString();
                    if (split[i].equals(value)) {
                        // 3. 修改副本注解的sort值(不影响原始注解)
                        InvocationHandler invocationHandler = Proxy.getInvocationHandler(copiedExcelField);
                        Field memberValues = invocationHandler.getClass().getDeclaredField("memberValues");
                        memberValues.setAccessible(true);
                        Map<String, Object> memberValuesMap = (Map) memberValues.get(invocationHandler);
                        memberValuesMap.put("sort", i); // 此处修改的是副本的sort
                        
                        // 4. 将副本注解放入新的Object数组,加入结果列表
                        Object[] copiedObjects = new Object[]{copiedExcelField, objects[1]}; // 保留原始标识值
                        annotationListResult.add(copiedObjects);
                    }
                }
            }
        }
    }
    return annotationListResult;
}
 

关键原理说明

  1. 注解的单例特性:Java 中,同一个注解在类加载后是单例的(全局唯一实例)。直接修改其属性会导致所有线程看到的sort值都被改变,引发并发污染。
  2. 副本隔离:通过AnnotationCopyUtil为每个需要修改的注解创建副本(新的代理实例),每个副本的sort值修改仅影响当前线程的当前操作,原始注解的sort值始终保持初始状态。
  3. 线程安全:副本注解是线程私有(每个线程操作自己的副本),避免了多线程并发修改的冲突,保证原始类中注解的初始sort值不被改变。

额外建议

  • ExcelUtil.getAnnotationList方法内部缓存了注解列表,建议确保缓存的是原始注解(未被修改的),避免缓存被污染。
  • 可在测试环境模拟多线程并发调用该方法,通过日志打印原始注解的sort值,验证是否已解决污染问题。
通过以上修改,既能满足动态调整sort值的需求,又能保证并发场景下原始注解属性的一致性。

--------------------------------------------------------------------------------------------------------

--------------------------------------------------------------------------------------------------------

--------------------------------------------------------------------------------------------------------

--------------------------------------------------------------------------------------------------------

posted @ 2025-11-18 13:52  hanease  阅读(7)  评论(0)    收藏  举报