开发中常用到 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、补充:
标签:return,自定义,MethodParameter,Easy,Poi,new,null,parameter,public From: https://www.cnblogs.com/Alay/p/17491422.html