虽然 EasyExcel 很香,但别再自己手写读写逻辑了

EasyExcel是一款由阿里开源的 Excel 处理工具。相较于原生的Apache POI,它可以更优雅、快速地完成 Excel 的读写功能,同时更加地节约内存。即使 EasyExcel 已经很优雅了,但面向 Excel 文档的读写逻辑几乎千篇一律,笔者索性将这些模板化的逻辑抽离出来,该组件已经发布到 maven 中央仓库,感兴趣的朋友可以体验一下。

1 快速上手

1.1 引入依赖

<dependency>
	<groupId>io.github.dk900912</groupId>
	<artifactId>easyexcel-spring-boot-starter</artifactId>
	<version>0.0.2</version>
</dependency>

1.2 导入与导出

@Validated
@RestController
@RequestMapping(path = "/easyexcel")
public class ExcelController {
    @PostMapping(path = "/v1/upload")
    public ResponseEntity<String> upload(@RequestExcel @Valid List<User> users) {
        return ResponseEntity.ok("OK");
    }

    @ResponseExcel(name="程序猿", sheetName = @Sheet(name="杜小头"), suffix = ExcelTypeEnum.XLSX)
    @GetMapping(path = "/v1/export")
    public List<User> export() {
        User user = User.builder().name("暴风赤红")
                .birth(LocalDate.now()).address("江苏省苏州市科技城昆仑山路58号")
                .build();
        return ImmutableList.of(user);
    }

    @ResponseExcel(name="templates/程序猿.xlsx", scene = TEMPLATE)
    @GetMapping(path = "/v1/template")
    public void template() {}
}

2 实现原理

一切 Java 程序都是基于 Thread 的,当一个 HTTP 请求到达后,Servlet Container 会从其线程池中捞出一个线程来处理该 HTTP 请求。具体地,该 HTTP 请求首先到达 Servlet Container 的FilterChain中;然后,FilterChain 将该 HTTP 请求委派给DispatcherServlet处理,而 DispatcherServlet 恰恰就是 Spring MVC 的门户。在 Spring MVC 中,所有 HTTP 请求都由 DispatcherServlet 进行路由分发。大致流程下图所示。

spring_mvc_execution_sequence.png

DispatcherServlet 在 HandlerMapping 的帮助下可以快速匹配到最终的 Controller,由于 Controller 大多由@RequestMapping注解标注,那么RequestMappingHandlerMapping最终脱颖而出。RequestMappingHandlerMapping 会将 HTTP 请求映射到一个HandlerExecutionChain实例中,每一个 HandlerExecutionChain 实例的内部维护了HandlerMethodList<HandlerInterceptor>。其中,HandlerMethod 实例持有一个Object类型的 bean 变量和java.lang.reflect.Method类型的 method 变量,bean 和 method 这俩成员变量组合起来最终可以确定究竟由哪一个 Controller 中的某一方法来处理当前 HTTP 请求。

此时已经知道目标方法了,那直接反射执行目标方法?是不可以的,因为通过反射来执行目标方法需要有参数才行,此外还需要对目标方法的执行结果进行加工处理。既然 HandlerMapping 没有解析请求体和处理目标执行结果的能力,只能再引入一层适配器了,它就是 HandlerAdapter。在 Spring MVC 所提供的若干种 HandlerAdapter 中,能够适配 HandlerMethod 的只有RequestMappingHandlerAdapter;RequestMappingHandlerAdapter 实现了InitializingBean接口,用于初始化HandlerMethodArgumentResolverComposite类型的 argumentResolvers 成员变量和HandlerMethodReturnValueHandlerComposite类型的 returnValueHandlers 成员变量,Composite 后缀表明这俩成员变量均是一种复合类,argumentResolvers 持有数十种HandlerMethodArgumentResolver类型的方法参数解析器,而 returnValueHandlers 则持有数十种HandlerMethodReturnValueHandler类型的方法返回值解析器。

重点来了!首先,我们需要一个实现 HandlerMethodArgumentResolver 接口的方法参数解析器,该解析器主要用于解析@RequestExcel注解,以读取 Excel 文档;此外,我们还需要一个实现 HandlerMethodReturnValueHandler 接口的方法返回值解析器,该解析器主要用于解析@ResponseExcel注解,以将目标方法所返回的数据写入到 Excel 文档中;最后,将这两个自定义的解析器分别添加到 RequestMappingHandlerAdapter 中的 argumentResolvers 与 returnValueHandlers 这俩成员变量中。

在 Spring MVC 中,由RequestResponseBodyMethodProcessor负责处理@RequestBody@ResponseBody 注解。基于这一事实,笔者也没有单独设计两个解析器来分别应对 @RequestExcel 与 @ResponseExcel 注解,而是合二为一。

2.1 @RequestExcel 与 @ResponseExcel 解析器

public class RequestResponseExcelMethodProcessor implements HandlerMethodArgumentResolver,
        HandlerMethodReturnValueHandler {

    private final ResourceLoader resourceLoader;

    public RequestResponseExcelMethodProcessor(ResourceLoader resourceLoader) {
        this.resourceLoader = resourceLoader;
    }

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

    @Override
    public boolean supportsReturnType(MethodParameter returnType) {
        return returnType.hasMethodAnnotation(ResponseExcel.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
                                  NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        parameter = parameter.nestedIfOptional();
        Object data = readWithMessageConverters(webRequest, parameter);
        String name = Conventions.getVariableNameForParameter(parameter);

        if (binderFactory != null) {
            WebDataBinder binder = binderFactory.createBinder(webRequest, data, name);
            if (data != null) {
                validateIfApplicable(binder, parameter);
                if (binder.getBindingResult().hasErrors()) {
                    throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
                }
            }
            if (mavContainer != null) {
                mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
            }
        }

        return data;
    }

    @Override
    public void handleReturnValue(Object returnValue, MethodParameter returnType,
                                  ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
        // There is no need to render view
        mavContainer.setRequestHandled(true);
        writeWithMessageConverters(returnValue, returnType, webRequest);
    }

    // +----------------------------------------------------------------------------+
    // |                            private method for read                         |
    // +----------------------------------------------------------------------------+

    protected <T> Object readWithMessageConverters(NativeWebRequest webRequest, MethodParameter parameter)
            throws IOException, UnsatisfiedMethodSignatureException {
        HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
        Assert.state(servletRequest != null, "No HttpServletRequest");

        return readWithMessageConverters(servletRequest, parameter);
    }

    protected Object readWithMessageConverters(HttpServletRequest servletRequest, MethodParameter parameter)
            throws IOException, UnsatisfiedMethodSignatureException {
        Class<?> targetClass = getArgParamOrReturnValueClass(parameter);

        RequestExcel requestExcel = parameter.getParameterAnnotation(RequestExcel.class);
        EmptyReadListener<?> emptyReadListener = new EmptyReadListener<>();
        InputStream inputStream;
        if (servletRequest instanceof MultipartRequest) {
            inputStream = ((MultipartRequest) servletRequest)
                    .getMultiFileMap()
                    .values()
                    .stream()
                    .flatMap(Collection::stream)
                    .findFirst()
                    .map(multipartFile -> {
                        try {
                            return multipartFile.getInputStream();
                        } catch (IOException e) {
                            return null;
                        }
                    })
                    .get();
        } else {
            inputStream = servletRequest.getInputStream();
        }
        EasyExcel.read(inputStream, targetClass, emptyReadListener)
                .headRowNumber(requestExcel.headRow())
                .sheet()
                .doRead();

        return emptyReadListener.getData();
    }

    protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
        Annotation[] annotations = parameter.getParameterAnnotations();
        for (Annotation ann : annotations) {
            Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann);
            if (validationHints != null) {
                binder.validate(validationHints);
                break;
            }
        }
    }

    // +----------------------------------------------------------------------------+
    // |                            private method for write                        |
    // +----------------------------------------------------------------------------+

    protected <T> void writeWithMessageConverters(Object value, MethodParameter returnType, NativeWebRequest webRequest)
            throws IOException, HttpMessageNotWritableException, UnsatisfiedMethodSignatureException {

        HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class);
        Assert.state(response != null, "No HttpServletResponse");

        ResponseExcel responseExcel = returnType.getMethodAnnotation(ResponseExcel.class);
        ExcelTypeEnum excelType = responseExcel.suffix();
        String name = responseExcel.name();
        Sheet sheet = responseExcel.sheetName();
        Scene scene = responseExcel.scene();

        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
        if (TEMPLATE.equals(scene)) {
            response.setHeader("Content-disposition",
                    "attachment;filename=" + URLEncoder.encode(name.substring(name.indexOf("/") + 1), StandardCharsets.UTF_8.name()));
            BufferedInputStream bufferedInputStream =
                    new BufferedInputStream(resourceLoader.getResource(CLASSPATH_URL_PREFIX + name).getInputStream());
            BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(response.getOutputStream());
            FileCopyUtils.copy(bufferedInputStream, bufferedOutputStream);
        } else {
            response.setHeader("Content-disposition",
                    "attachment;filename=" + URLEncoder.encode(name, StandardCharsets.UTF_8.name()) + excelType.getValue());
            Class<?> targetClass = getArgParamOrReturnValueClass(returnType);
            EasyExcel.write(response.getOutputStream(), targetClass)
                    .excelType(excelType)
                    .sheet(sheet.name())
                    .doWrite((Collection<?>) value);
        }
    }

    private Class<?> getArgParamOrReturnValueClass(MethodParameter target) throws UnsatisfiedMethodSignatureException {
        ResolvableType resolvableType = ResolvableType.forMethodParameter(target);
        if (!Collection.class.isAssignableFrom(resolvableType.resolve())) {
            throw new UnsatisfiedMethodSignatureException("Unsatisfied Method Signature");
        }
        ResolvableType generic = resolvableType.getGeneric(0);
        if (Collection.class.isAssignableFrom(generic.resolve()) || Map.class.isAssignableFrom(generic.resolve())) {
            throw new UnsatisfiedMethodSignatureException("Unsatisfied Method Signature");
        }

        return generic.resolve();
    }
}

2.2 RequestMappingHandlerAdapter 后置处理器

public class RequestMappingHandlerAdapterPostProcessor implements BeanPostProcessor,
        PriorityOrdered, ResourceLoaderAware {

    private ResourceLoader resourceLoader;

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        if (!supports(bean)) {
            return bean;
        }

        RequestMappingHandlerAdapter requestMappingHandlerAdapter = (RequestMappingHandlerAdapter) bean;
        List<HandlerMethodArgumentResolver> argumentResolvers = requestMappingHandlerAdapter.getArgumentResolvers();
        List<HandlerMethodReturnValueHandler> returnValueHandlers = requestMappingHandlerAdapter.getReturnValueHandlers();
        Assert.notEmpty(argumentResolvers,
                "RequestMappingHandlerAdapter's argument resolver is empty, this is illegal state");
        Assert.notEmpty(returnValueHandlers,
                "RequestMappingHandlerAdapter's return-value handler is empty, this is illegal state");

        List<HandlerMethodArgumentResolver> copyArgumentResolvers = new ArrayList<>(argumentResolvers);
        RequestResponseExcelMethodProcessor argumentResolver4RequestExcel = new RequestResponseExcelMethodProcessor(null);
        copyArgumentResolvers.add(0, argumentResolver4RequestExcel);
        requestMappingHandlerAdapter.setArgumentResolvers(Collections.unmodifiableList(copyArgumentResolvers));

        List<HandlerMethodReturnValueHandler> copyReturnValueHandlers = new ArrayList<>(returnValueHandlers);
        RequestResponseExcelMethodProcessor returnValueHandler4ResponseExcel = new RequestResponseExcelMethodProcessor(resourceLoader);
        copyReturnValueHandlers.add(0, returnValueHandler4ResponseExcel);
        requestMappingHandlerAdapter.setReturnValueHandlers(Collections.unmodifiableList(copyReturnValueHandlers));

        return requestMappingHandlerAdapter;
    }

    @Override
    public int getOrder() {
        return Ordered.LOWEST_PRECEDENCE;
    }

    @Override
    public void setResourceLoader(ResourceLoader resourceLoader) {
        this.resourceLoader = resourceLoader;
    }

    private boolean supports(Object bean) {
        return bean instanceof RequestMappingHandlerAdapter;
    }
}

3 总结

目前该版本仅支持针对单个 Excel 文档的导入与导出,所以由 @RequestExcel 注解修饰的方法参数必须是一个List类型,而由 @RequestExcel 注解修饰的方法返回类型也必须是一个List类型,否则将抛出UnsatisfiedMethodSignatureException类型的自定义异常。

坦白来说,该组件的设计初衷只是为了帮助大家从公式化、模板化的Excel 读写逻辑中解放出来,从而专注于核心业务逻辑的开发,并不是为了增强 EasyExcel,后续也不会朝着这一方向演进。

参考

  1. github.com/dk900912/ea…

猜你喜欢

转载自juejin.im/post/7112446584083709988