基于Easy-Poi 的自定义 ArgumentResolver 和 ReturnValueHandler

发布时间 2023-06-19 16:18:15作者: Vermeer

开发中常用到 Excel 的导入导出,为了方便快速的使用,让使用者使用 Excel 像使用 JSON 一样便捷 (@RequestBody @ResponsBody )

所以,是否可以自定义编写类似功能的注解  @RequestExcel  和 @ResponseExcel  

一、实现思路:

根据 Mvc 的参数转换 和返回值处理机制实现,excel 相关工具本案例使用了 Easy-POI 

1、请求参数中

将客户端的 Excel 文件转换为 数据实体,MVC提供了扩展参数处理机制接口 HandlerMethodArgumentResolver, 源码中 HandlerMethodArgumentResolverComposite ,此类为 HandlerMethodArgumentResolver 接口的 委托类,委托管理了所有的 HandlerMethodArgumentResolver,

    @Override
    @Nullable
    public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
            NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {

        HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter);
        if (resolver == null) {
            throw new IllegalArgumentException("Unsupported parameter type [" +
                    parameter.getParameterType().getName() + "]. supportsParameter should be called first.");
        }
        return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
    }

    /**
     * Find a registered {@link HandlerMethodArgumentResolver} that supports
     * the given method parameter.
     */
    @Nullable
    private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
        HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
        if (result == null) {
            for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) {
                if (resolver.supportsParameter(parameter)) {
                    result = resolver;
                    this.argumentResolverCache.put(parameter, result);
                    break;
                }
            }
        }
        return result;
    }

所以编写相应的 参数解析器处理 Excel 入参类型即可,

2、响应客户端 Excel 文件

返回数据时将实体对象数据输出位  Excel 文件,MVC 中提供了返回值数据拦截处理机制 HandlerMethodReturnValueHandler ,其中  HandlerMethodReturnValueHandlerComposite 委托管理了所有的  HandlerMethodReturnValueHandler

    @Override
    public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
            ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {

        HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType);
        if (handler == null) {
            throw new IllegalArgumentException("Unknown return value type: " + returnType.getParameterType().getName());
        }
        handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
    }

    @Nullable
    private HandlerMethodReturnValueHandler selectHandler(@Nullable Object value, MethodParameter returnType) {
        boolean isAsyncValue = isAsyncReturnValue(value, returnType);
        for (HandlerMethodReturnValueHandler handler : this.returnValueHandlers) {
            if (isAsyncValue && !(handler instanceof AsyncHandlerMethodReturnValueHandler)) {
                continue;
            }
            if (handler.supportsReturnType(returnType)) {
                return handler;
            }
        }
        return null;
    }

所以编写相应的实现类进行扩展返回值数据处理即可

 

3、Excel 处理框架选择

本案例中使用的是 easy-poi  作为 Excel 处理框架,官网: http://doc.wupaas.com/docs/easypoi/easypoi-1c0u6ksp2r091

本案例比对 easy-poi 使用做详细讲解,请自补

个人建议: 个人建议 针对 Excel 如果有很多自行扩展处理的需求,请放弃使用 easy-poi,改用 easy-excel (官网:https://easyexcel.opensource.alibaba.com/ ),

原因: 阿里整合的框架考虑到多方面的自定义扩展接口,设计合理便捷,Easy-POI 想要扩展自定义功能就没那么丝滑,如果你项目中源码深度定制化的话,将会踩很多坑,而让你不得不妥协,要么放弃自己的设计,要么改写框架源码

有点,如果你开发中定制化数据处理,类型处理没那么高的要求,easy-poi 还是不错的选择的,支持各种华丽骚操作,模板支持也很哇塞

本案例由于是多早期的项目进行增强扩展,所以就不会引入 新框架 easy-excel ,沿用原有框架  easy-poi ,新的架构中使用 easy-excel 替代此封装,但属于私有项目,故不能分享 给大家,但是实现思路还是一样的。

 

二、实操实现

1、说明:

本案例中,仅仅是演示案例,并非真实上线项目案例,所以一切从简,表达上点到为止,抛砖引玉(真实生产项目中已经使用 easy-excel )

2、注解准备:

@RequestExcel

@Documented
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestExcel {
    // 文件名
    String name() default "file";
    // 表头在第几行
    int headRows() default 1;
    //  多 Sheet 导入导出  
    Sheet[] sheets() default {};
}

@ResponseExcel

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface ResponseExcel {
    // 文件名
    String name() default "file";
    // 多 Sheet 导入导出
    Sheet[] sheets() default {};
}

@Sheet

@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Sheet {
    // 实体类信息
    Class<?> entityClazz();
    // 表头在第几行
    int headRows() default 1;
}

 

3、编写参数解析器 和  返回值拦截器

参数解析器:核心是 WorkbookHandler

public class RequestExcelArgumentResolver implements HandlerMethodArgumentResolver {
    private final Logger logger = LoggerFactory.getLogger(this.getClass());
    /**
     * 处理 Workbook 数据的拦截器(基于 easy-poi, 可以根据需求替换为 easy-excel 的实现)
     */
    private final WorkbookHandler<?> workbookHandler = new DelegatingWorkbookHandler(new SingleSheetHandler(), new ManySheetHandler());


    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(RequestExcel.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        Class<?> parameterType = parameter.getParameterType();
        if (!parameterType.isAssignableFrom(List.class) && !parameterType.isAssignableFrom(Map.class)) {
            throw new IllegalArgumentException(
                    "Excel upload request resolver error, @RequestExcel parameter must be List or Map" + parameterType);
        }
        // Excel 注解信息
        RequestExcel requestExcel = parameter.getParameterAnnotation(RequestExcel.class);

        // Request
        HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);

        InputStream inputStream;
        if (request instanceof MultipartRequest) {
            MultipartFile file = ((MultipartRequest) request).getFile(requestExcel.name());
            if (null == file) {
                logger.error("api request parameter definition file name:[{}]", requestExcel.name());
                throw new ExcelException("request file name must be [" + requestExcel.name() + "]");
            }
            inputStream = file.getInputStream();
        } else {
            assert request != null;
            inputStream = request.getInputStream();
        }
        // 调用 easy-poi 处理 file 文件转换为 实体对象
        return workbookHandler.importExcel(parameter, inputStream, requestExcel);
    }
}

 

返回数据拦截器:核心是 WorkbookHandler

public class ResponseExcelReturnValueHandler implements HandlerMethodReturnValueHandler {
    private final Logger logger = LoggerFactory.getLogger(this.getClass());
    /**
     * 处理 Workbook 数据的拦截器(基于 easy-poi, 可以根据需求替换为 easy-excel 的实现)
     */
    private final WorkbookHandler<Object> workbookHandler = new DelegatingWorkbookHandler(new SingleSheetHandler(), new ManySheetHandler());


    @Override
    public boolean supportsReturnType(MethodParameter returnType) {
        return returnType.getMethodAnnotation(ResponseExcel.class) != null;
    }

    @Override
    public void handleReturnValue(Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
        HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class);
        Assert.state(response != null, "No HttpServletResponse");
        // 注解信息
        ResponseExcel responseExcel = returnType.getMethodAnnotation(ResponseExcel.class);
        Assert.state(responseExcel != null, "No @ResponseExcel");
        mavContainer.setRequestHandled(true);

        // 调用 easy-poi 处理 实体数据写入 workbook
        Workbook workbook = workbookHandler.exportExcel(returnType, response, responseExcel, returnValue);

        String fileName = StrUtil.isNotBlank(responseExcel.name()) ? responseExcel.name() : "file";

        // 下载 Excel
        response.reset();
        response.setHeader(HttpHeaders.CONTENT_TYPE, "application/vnd.ms-excel");
        response.setHeader(HttpHeaders.CONTENT_DISPOSITION,
                String.format("attachment; filename=%s.xlsx", URLEncoder.encode(fileName, StandardCharsets.UTF_8)));
        try {
            workbook.write(response.getOutputStream());
        } catch (IOException e) {
            logger.error(String.format("data export error, message: %s", e.getMessage()));
        }
    }
}

 

4、注册 参数解析器 和 返回数据拦截器

注意:需要将自定义的参数处理和 返回值处理优先级放到前面,否则会被其他拦截器抢先拦截处理了

@Configuration
@RequiredArgsConstructor
public class SmartPoiAutoConfiguration implements InitializingBean {

    private final RequestMappingHandlerAdapter requestMappingHandlerAdapter;

    @Override
    public void afterPropertiesSet() {
        // 替换原有的返回数据解析处理拦截器
        List<HandlerMethodReturnValueHandler> handlers = requestMappingHandlerAdapter.getReturnValueHandlers();
        List<HandlerMethodReturnValueHandler> newHandlers = new ArrayList<>();
        // 让我们自己编写的拦截器放在首位,避免被 @ResponseBody(@RestController) 抢占了先机
        newHandlers.add(new ResponseExcelReturnValueHandler());
        assert handlers != null;
        newHandlers.addAll(handlers);
        requestMappingHandlerAdapter.setReturnValueHandlers(newHandlers);


        // 替换原有的参数解析器
        List<HandlerMethodArgumentResolver> resolvers = requestMappingHandlerAdapter.getArgumentResolvers();
        List<HandlerMethodArgumentResolver> newResolvers = new ArrayList<>();
        // 让我们自己编写的拦截器放在首位
        newResolvers.add(new RequestExcelArgumentResolver());
        assert resolvers != null;
        newResolvers.addAll(resolvers);
        requestMappingHandlerAdapter.setArgumentResolvers(newResolvers);
    }
}

5、Excel 文件与 实体对象相互转换处理拦截器:

public interface WorkbookHandler<T> {
    /**
     * 处理类型判断
     */
    boolean supports(MethodParameter parameter, RequestExcel requestExcel);
    /**
     * 处理类型判断
     */
    boolean supports(MethodParameter parameter, ResponseExcel responseExcel);
    /**
     * 导入 Excel
     */
    T importExcel(MethodParameter parameter, InputStream is, RequestExcel requestExcel) throws Exception;
    /**
     * 导出Excel*/
    Workbook exportExcel(MethodParameter returnType, HttpServletResponse response, ResponseExcel responseExcel, T data);
}

实现类 SingleSheetHandler   正对单个 Sheet 文件上传和下载的处理器 (以下实现是 基于 easy-poi )

public class SingleSheetHandler implements WorkbookHandler<Collection<Object>> {

    @Override
    public boolean supports(MethodParameter parameter, RequestExcel requestExcel) {
        return this.supports(parameter);
    }

    @Override
    public boolean supports(MethodParameter parameter, ResponseExcel responseExcel) {
        return this.supports(parameter);
    }

    @Override
    public Collection<Object> importExcel(MethodParameter parameter, InputStream is, RequestExcel requestExcel) throws Exception {
        // 获取目标类型
        Class<?> entityClazz = ResolvableType.forMethodParameter(parameter).getGeneric(0).resolve();
        // 导入参数
        ImportParams importParam = new ImportParams();
        importParam.setHeadRows(requestExcel.headRows());

        return ExcelImportUtil.importExcel(is, entityClazz, importParam);
    }


    @Override
    public Workbook exportExcel(MethodParameter returnType, HttpServletResponse response, ResponseExcel responseExcel, Collection<Object> data) {
        Class<?> entityClazz = ResolvableType.forMethodParameter(returnType).getGeneric(0).resolve();
        ExcelTarget excelTarget = entityClazz.getAnnotation(ExcelTarget.class);

        String title = null;
        // 如果实体类存在 @ExcelTarget 注解,则从注解中获取标题和 sheet 名称
        if (null != excelTarget) title = excelTarget.value();

        ExportParams exportParam = new ExportParams(title, null == title ? "sheet0" : title);
        return ExcelExportUtil.exportExcel(exportParam, entityClazz, data);
    }


    private boolean supports(MethodParameter parameter) {
        // 类型必须是 Collection 类型
        Class<?> parameterType = parameter.getParameterType();
        if (!List.class.isAssignableFrom(parameterType)) return false;

        Class<?> entityClazz = ResolvableType.forMethodParameter(parameter).getGeneric(0).resolve();
        assert entityClazz != null;
        return !Collection.class.isAssignableFrom(entityClazz);
    }

}

 

ManySheetHandler 多个 Sheet 批量导入

public class ManySheetHandler implements WorkbookHandler<Map<Class<?>, Collection<Object>>> {
    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    @Override
    public boolean supports(MethodParameter parameter, RequestExcel requestExcel) {
        return this.supports(parameter);
    }

    @Override
    public boolean supports(MethodParameter parameter, ResponseExcel responseExcel) {
        return this.supports(parameter);
    }

    @Override
    public Map<Class<?>, Collection<Object>> importExcel(MethodParameter parameter, InputStream is, RequestExcel requestExcel) throws Exception {
        Sheet[] sheets = requestExcel.sheets();

        // ExcelImportUtil.importExcelMore() 支持的 多 sheet 实体类必须是同一个类型,所以这里使用了 拷贝 InputStream 进行多次读取
        ByteArrayOutputStream bos = this.cloneInputStream(is);
        Map<Class<?>, Collection<Object>> result = new HashMap<>();
        try {
            for (int i = 0; i < sheets.length; i++) {
                Sheet sheet = sheets[i];
                Class<?> entityClazz = sheet.entityClazz();
                // 导入参数
                ImportParams importParam = new ImportParams();

                importParam.setStartSheetIndex(i);
                importParam.setHeadRows(sheet.headRows());
                
                // ExcelImportUtil.importExcelMore() 支持的 多 sheet 实体类必须是同一个类型,所以这里使用了 拷贝 InputStream 进行多次读取
                ExcelImportResult<Object> importExcel = ExcelImportUtil.importExcelMore(new ByteArrayInputStream(bos.toByteArray()), entityClazz, importParam);
                List<Object> data = importExcel.getList();
                result.put(entityClazz, data);
            }
            return result;
        } finally {
            bos.close();
        }
    }

    @Override
    public Workbook exportExcel(MethodParameter returnType, HttpServletResponse response, ResponseExcel responseExcel, Map<Class<?>, Collection<Object>> bookData) {
        // 批量导出的数据,多个 sheet
        List<Map<String, Object>> batchExports = new ArrayList<>();
        Sheet[] sheets = responseExcel.sheets();
        for (int i = 0; i < sheets.length; i++) {
            Sheet sheet = sheets[i];
            Class<?> entityClazz = sheet.entityClazz();
            String title = null;
            ExcelTarget target = entityClazz.getAnnotation(ExcelTarget.class);

            // 如果实体类存在 @ExcelTarget 注解,则从注解中获取标题和 sheet 名称
            if (null != target) title = target.value();

            ExportParams exportParam = new ExportParams(title, null == title ? "sheet" + i : title);
            Collection<Object> data = bookData.get(sheet.entityClazz());
            Map<String, Object> sheetMap = new HashMap<>();
            // title的参数为ExportParams类型,目前仅仅在ExportParams中设置了sheetName
            sheetMap.put("title", exportParam);
            // sheet中要填充得数据
            sheetMap.put("data", data);
            // 模版导出对应得实体类型,即包含了List的对象
            sheetMap.put("entity", sheet.entityClazz());
            batchExports.add(sheetMap);
        }
        return ExcelExportUtil.exportExcel(batchExports, ExcelType.XSSF);
    }

    private boolean supports(MethodParameter parameter) {
        Class<?> parameterType = parameter.getParameterType();
        // 类型必须是 Map 类型
        if (!Map.class.isAssignableFrom(parameterType)) return false;

        // 泛型必须是 Collection 类型
        Class<?> dataClazz = ResolvableType.forMethodParameter(parameter).getGeneric(1).resolve();
        assert dataClazz != null;
        // 泛型里边也必须是 Collection 类型
        return List.class.isAssignableFrom(dataClazz);
    }


    private ByteArrayOutputStream cloneInputStream(InputStream is) {
        ByteArrayOutputStream baos;
        try {
            baos = new ByteArrayOutputStream();
            byte[] buffer = new byte[1024];
            int len;
            while ((len = is.read(buffer)) > -1) {
                baos.write(buffer, 0, len);
            }
            baos.flush();
        } catch (IOException e) {
            logger.error("cloneInputStream error");
            throw new ExcelException("InputStream copy ByteArrayOutputStream error", e);
        }
        return baos;
    }

}

 

WorkbookHandler 委托类, DelegatingWorkbookHandler,作用是负责管理 ManySheetHandler 和 SingleSheetHandler

public class DelegatingWorkbookHandler implements WorkbookHandler<Object> {
    private final List<WorkbookHandler> workbookHandlers;

    public DelegatingWorkbookHandler(WorkbookHandler... sheetHandlers) {
        Assert.notEmpty(sheetHandlers, "sheetHandlers not be empty");
        this.workbookHandlers = new ArrayList<>(sheetHandlers.length);
        for (WorkbookHandler sheetHandler : sheetHandlers) {
            this.workbookHandlers.add(sheetHandler);
        }
    }

    @Override
    public boolean supports(MethodParameter parameter, RequestExcel requestExcel) {
        return false;
    }

    @Override
    public boolean supports(MethodParameter parameter, ResponseExcel responseExcel) {
        return false;
    }

    @Override
    public Object importExcel(MethodParameter parameter, InputStream is, RequestExcel requestExcel) throws Exception {
        for (WorkbookHandler sheetHandler : workbookHandlers) {
            if (sheetHandler.supports(parameter, requestExcel)) {
                return sheetHandler.importExcel(parameter, is, requestExcel);
            }
        }
        return null;
    }

    @Override
    public Workbook exportExcel(MethodParameter returnType, HttpServletResponse response, ResponseExcel responseExcel, Object data) {
        for (WorkbookHandler workbookHandler : workbookHandlers) {
            if (workbookHandler.supports(returnType, responseExcel)) {
                return workbookHandler.exportExcel(returnType, response, responseExcel, data);
            }
        }
        return null;
    }
}

 

三、配置注入

请根据自己项目风格进行注入 Spring 中使用,比如 spring.factories(spring-boot2.7.x 中已经废除此法)-- > 中注入 SmartPoiAutoConfiguration  或者使用 编写注解开关  @EnableXxxExcel 方式引入 SmartPoiAutoConfiguration 注入使用

四、使用

1、@RequestExcel :

支持单 sheet 导入,多 Sheet 导入,单 sheet 导出时接收数据约定为 List , 多 Sheet导入时 接收数据类型为 Map<Class<?>, List<?>>

 

2、@ResponsExcel :

支持单 sheet 导出,多 Sheet 导出,单 sheet 导出时返回数据约定为 List , 多 Sheet导出时 返回数据类型为 Map<Class<?>, List<?>>

 3、示例实体:

 4、补充: