EasyExcel 是一款由阿里开源的 Excel 处理工具。相较于原生的Apache POI
,它可以更优雅、快速地完成 Excel 的读写功能,同时更加地节约内存。即使 EasyExcel 已经很优雅了,但面向 Excel 文档的读写逻辑几乎千篇一律,笔者索性将这些模板化的逻辑抽离出来,该组件已经发布到 maven 中央仓库,感兴趣的朋友可以体验一下。
目前仅支持针对单个 Excel 文档的导入与导出(支持多Sheet),所以由 @RequestExcel 注解修饰的方法参数必须是一个List<List<>>类型,而由 @ResponseExcel 注解修饰的方法返回类型也必须是一个List<List<>>类型,否则将抛出异常。
easyexcel 组件组要自行引入,强制使用 3.3+ 版本。
<dependency>
<groupId>io.github.dk900912</groupId>
<artifactId>easyexcel-spring-boot-starter</artifactId>
<version>1.0.1</version>
</dependency>
@RestController
@RequestMapping(path = "/easyexcel")
public class ExcelController {
@PostMapping(path = "/v1/upload")
public ResponseEntity<String> v1upload(
@RequestExcel(sheets = {
@Sheet(index = 0, headClazz = User.class, headRowNumber = 1),
@Sheet(index = 1, headClazz = User.class, headRowNumber = 1),
@Sheet(index = 2, headClazz = User.class, headRowNumber = 1)}
)
List<List<User>> users) {
return ResponseEntity.ok("OK");
}
@PostMapping(path = "/v2/upload")
public ResponseEntity<String> v2upload(
@RequestExcel(sheets = {
@Sheet(index = 0, headClazz = User.class, headRowNumber = 1),
@Sheet(index = 1, headClazz = Admin.class, headRowNumber = 1)}
)
List<List<Object>> data) {
return ResponseEntity.ok("OK");
}
@ResponseExcel(
name="程序猿",
sheets = {
@Sheet(name = "sheet-0", headClazz = User.class),
@Sheet(name = "sheet-1", headClazz = User.class),
@Sheet(name = "sheet-2", headClazz = User.class)
},
suffix = ExcelTypeEnum.XLSX)
@GetMapping(path = "/v1/export")
public List<List<User>> v1export() {
List<User> userList = Lists.newArrayList();
for (int i = 0; i < 10000; i++) {
User user = User.builder().name("暴风赤红" + (i+1))
.birth(LocalDate.now()).address("江苏省苏州市科技城昆仑山路58号").sex(Sex.MALE)
.build();
userList.add(user);
}
return ImmutableList.of(userList, userList, userList);
}
@ResponseExcel(
name="程序猿",
sheets = {
@Sheet(name = "sheet-0", headClazz = User.class),
@Sheet(name = "sheet-1", headClazz = Admin.class)
},
suffix = ExcelTypeEnum.XLS)
@GetMapping(path = "/v2/export")
public List<List<?>> v2export() {
List<User> userList = Lists.newArrayList();
List<Admin> adminList = Lists.newArrayList();
for (int i = 0; i < 10000; i++) {
User user = User.builder().name("暴风赤红" + (i+1))
.birth(LocalDate.now()).address("江苏省苏州市科技城昆仑山路58号").sex(Sex.MALE)
.build();
userList.add(user);
Admin admin = Admin.builder().name("擎天柱" + (i+1))
.birth(LocalDate.now()).address("江苏省苏州市科技城昆仑山路68号").sex(Sex.MALE)
.build();
adminList.add(admin);
}
List<List<?>> responseData = Lists.newArrayList();
responseData.add(userList);
responseData.add(adminList);
return responseData;
}
@ResponseExcel(name="templates/tem.xlsx", scene = TEMPLATE)
@GetMapping(path = "/v1/template")
public void template() {}
}
一般无需配置
spring.easy-excel.enabled=true
spring.easy-excel.converter.media-types[0]=application/octet-stream
spring.easy-excel.template.location=classpath:
在非模板导出场景下,导出文件名可以显式指定,也可以不指定,此时会默认使用基于 UUID 的文件名生成策略,如果不满足大家的需求,可以自行实现FileNameGenerator
策略接口,然后追加配置项,如下所示:
spring.easy-excel.name.generator=a.b.c.CustomFileNameGenerator
同时内置了两个文件名生成器供大家使用,分别是:
- io.github.dk900912.easyexcel.support.TimestampFileNameGenerator
- io.github.dk900912.easyexcel.support.LocalDateTimeFileNameGenerator
依托于 Spring 内部 MethodValidationInterceptor
的能力,配合@Validated
和@Valid
注解即可实现。
@Validated
@RestController
@RequestMapping(path = "/easyexcel")
public class ExcelController {
@PostMapping(path = "/v1/upload")
public ResponseEntity<String> v1upload(
@RequestExcel(sheets = {
@Sheet(index = 0, headClazz = User.class, headRowNumber = 1),
@Sheet(index = 1, headClazz = User.class, headRowNumber = 1),
@Sheet(index = 2, headClazz = User.class, headRowNumber = 1)}
)
List<List<@Valid User>> users, @RequestParam("id") String id) {
System.out.println(users);
System.out.println(id);
return ResponseEntity.ok("OK");
}
}
异常信息如下:
jakarta.validation.ConstraintViolationException: v1upload.users[1].<list element>[0].name: name不能为空, v1upload.users[0].<list element>[0].name: name不能为空
at org.springframework.validation.beanvalidation.MethodValidationInterceptor.invoke(MethodValidationInterceptor.java:138) ~[spring-context-6.0.9.jar:6.0.9]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) ~[spring-aop-6.0.9.jar:6.0.9]
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:750) ~[spring-aop-6.0.9.jar:6.0.9]
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:702) ~[spring-aop-6.0.9.jar:6.0.9]
at io.github.xiaotou.easyexcel.ExcelController$$SpringCGLIB$$0.v1upload(<generated>) ~[classes/:na]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na]
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na]
at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:207) ~[spring-web-6.0.9.jar:6.0.9]
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:152) ~[spring-web-6.0.9.jar:6.0.9]
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:118) ~[spring-webmvc-6.0.9.jar:6.0.9]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:884) ~[spring-webmvc-6.0.9.jar:6.0.9]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:797) ~[spring-webmvc-6.0.9.jar:6.0.9]
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-6.0.9.jar:6.0.9]
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1081) ~[spring-webmvc-6.0.9.jar:6.0.9]
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:974) ~[spring-webmvc-6.0.9.jar:6.0.9]
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1011) ~[spring-webmvc-6.0.9.jar:6.0.9]
at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:914) ~[spring-webmvc-6.0.9.jar:6.0.9]
at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:590) ~[tomcat-embed-core-10.1.8.jar:6.0]
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:885) ~[spring-webmvc-6.0.9.jar:6.0.9]
at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658) ~[tomcat-embed-core-10.1.8.jar:6.0]
从上述异常信息来看,出现了两次“name不能为空”,这说明 Jakarta Bean Validation 的实现方(Hibernate)没有开启fail fast
机制,可以通过以下方式开启:
spring.easy-excel.validation.fail-fast=false
否则本组件将自动开启fail fast
机制,如下:
public class FailFastValidationConfigurationCustomizer implements ValidationConfigurationCustomizer {
@Override
public void customize(Configuration<?> configuration) {
configuration.addProperty("hibernate.validator.fail_fast", "true");
}
}