此项目底层基于 FastExcel 实现 Excel 的读写。
此项目1.2.10 & 3.1.3 版本及之前版本,底层基于
EasyExcel实现 Excel 的读写。
FastExcel是一个基于Java的简单、省内存的读写Excel的开源项目。在尽可能节约内存的情况下支持读写百M的Excel。 64M内存1分钟内读取75M(46W行25列)的Excel,当然还有急速模式能更快,但是内存占用会在100M多一点

| 版本 | 支持 |
|---|---|
| 3.x.x | 适配 SpringBoot3.x |
| 1.x.x | 适配 SpringBoot2.x |
<dependency>
<groupId>com.kangaroohy</groupId>
<artifactId>excel-spring-boot-starter</artifactId>
<version>${lastVersion}</version>
</dependency>
@PostMapping("/upload")
public void upload(@RequestExcel List<DemoData> dataList,BindingResult bindingResult){
// JSR 303 校验通用校验获取失败的数据
List<ErrorMessage> errorMessageList=(List<ErrorMessage>)bindingResult.getTarget();
}
@Data
public class Demo {
@ExcelProperty(index = 0)
private String username;
@ExcelProperty(index = 1)
private String password;
}
只需要在 Controller 层返回 List 并增加 @ResponseExcel注解即可
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ResponseExcel {
String name() default "";
ExcelTypeEnum suffix() default ExcelTypeEnum.XLSX;
String password() default "";
Sheet[] sheets() default @Sheet(sheetName = "sheet1");
boolean inMemory() default false;
String template() default "";
String[] include() default {};
String[] exclude() default {};
Class<? extends WriteHandler>[] writeHandler() default {};
Class<? extends Converter>[] converter() default {};
Class<? extends HeadGenerator> headGenerator() default HeadGenerator.class;
boolean i18nHeader() default false;
boolean fill() default false;
String waterMark() default "";
}
sheet, 全部字段导出@ResponseExcel(name = "test", sheets = @Sheet(sheetName = "testSheet1"))
@GetMapping("/e1")
public List<DemoData> e1(){
List<DemoData> dataList=new ArrayList<>();
for(int i=0;i< 100;i++){
DemoData data=new DemoData();
data.setUsername("tr1"+i);
data.setPassword("tr2"+i);
dataList.add(data);
}
return dataList;
}
// 实体对象
@Data
public class DemoData {
private String username;
private String password;
}

@Data
public class DemoData {
@ColumnWidth(50) // 定义宽度
@ExcelProperty("用户名") // 定义列名称
@ContentStyle(fillPatternType = FillPatternTypeEnum.SOLID_FOREGROUND, fillForegroundColor = 40)
private String username;
@ExcelProperty("密码")
private String password;
}

@Data
public class DemoData {
@ColumnWidth(50) // 定义宽度
@ExcelProperty("用户名") // 定义列名称
@ContentStyle(fillPatternType = FillPatternType.SOLID_FOREGROUND, fillForegroundColor = 40)
private String username;
@ExcelIgnore // 忽略这个字段
private String password;
}

@ResponseExcel(name = "kangaroohy", password = "kangaroohy")
@GetMapping("/e1")
public List<DemoData> e1(){
return list();
}

@ResponseExcel(name = "kangaroohy", sheets = {
@Sheet(sheetName = "第一个Sheet"),
@Sheet(sheetName = "第二个sheet")
})
@GetMapping("/e1")
public List<List<DemoData>>e1(){
List<List<DemoData>>lists=new ArrayList<>();
lists.add(list());
lists.add(list());
return lists;
}

这里两个 sheet 导出不同类型的对象,只导出 DemoData 中的 username 属性,且将 testData 中的 number 属性排除。
@Controller
@RequestMapping("public/excel")
public class ExportMultiSheetController {
@ResponseExcel(name = "不同Sheet的导出", sheets = {
@Sheet(sheetName = "demoData", includes = {"username"}),
@Sheet(sheetName = "testData", excludes = {"number"})
})
@GetMapping("/different-sheet")
public List<List> multiDifferent() {
List<List> lists = new ArrayList<>();
lists.add(demoDatalist());
lists.add(testDatalist());
return lists;
}
private List<DemoData> demoDatalist() {
List<DemoData> dataList = new ArrayList<>();
for (int i = 0; i < 100; i++) {
DemoData data = new DemoData();
data.setUsername("tr1" + i);
data.setPassword("tr2" + i);
dataList.add(data);
}
return dataList;
}
private List<TestData> testDatalist() {
List<TestData> dataList = new ArrayList<>();
for (int i = 0; i < 100; i++) {
TestData data = new TestData();
data.setStr("str" + i);
data.setNumber(i);
data.setLocalDateTime(LocalDateTime.now());
dataList.add(data);
}
return dataList;
}
// 实体对象
@Data
public static class DemoData {
private String username;
private String password;
}
@Data
public static class TestData {
private String str;
private Integer number;
@ColumnWidth(50) // 定义宽度
private LocalDateTime localDateTime;
}
}

测试实体类:
@Data
public class SimpleData {
@ExcelProperty("字符串标题")
private String string;
@ExcelProperty("日期标题")
private Date date;
@ExcelProperty("数字标题")
private Integer number;
// 忽略
@ExcelIgnore
private String ignore;
}
自定义头信息生成器:
注意需要实现
HeadGenerator接口,且注册为一个 spring bean.
@Component
public class SimpleDataHeadGenerator implements HeadGenerator {
@Override
public HeadMeta head(Class<?> clazz) {
HeadMeta headMeta = new HeadMeta();
headMeta.setHead(simpleDataHead());
// 排除 number 属性
headMeta.setIgnoreHeadFields(new HashSet<>(Collections.singletonList("number")));
return headMeta;
}
private List<List<String>> simpleDataHead() {
List<List<String>> list = new ArrayList<>();
List<String> head0 = new ArrayList<>();
head0.add("自定义字符串标题" + System.currentTimeMillis());
List<String> head1 = new ArrayList<>();
head1.add("自定义日期标题" + System.currentTimeMillis());
list.add(head0);
list.add(head1);
return list;
}
}
该头生成器,将固定返回 自定义字符串标题 和 自定义日期标题 两列头信息,实际使用时可根据业务动态处理,方便在一些权限控制时动态修改或者增删列头。
测试代码:
@RequestMapping("/head")
@RestController
public class ExcelHeadTestController {
@ResponseExcel(name = "customHead", headGenerator = SimpleDataHeadGenerator.class)
@GetMapping
public List<SimpleData> multi() {
List<SimpleData> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
SimpleData simpleData = new SimpleData();
simpleData.setString("str" + i);
simpleData.setNumber(i);
simpleData.setDate(new Date());
list.add(simpleData);
}
return list;
}
}

@RequestMapping("/water-mark")
@RestController
public class ExcelWaterMarkTestController {
@ResponseExcel(name = "用户记录", sheets = {
@Sheet(sheetName = "用户信息")
}, waterMark = "这是水印")
@GetMapping
public List<User> exportWithWaterMark() {
List<User> userList = new ArrayList<>();
for (int i = 0; i < 100; i++) {
userList.add(User.builder()
.id((long) i)
.username("username" + i)
.password("password" + i)
.build());
}
return userList;
}
}

国际化配置基于 Spring 的 MessageSource,开启国际化时,spring 容器中必须有一个 MessageSource 的 Bean。
具体 Spring
的国际化使用这里不再展开,想要了解的可以参看官方文档 Spring MessageSource 使用
首先在 resource 下,新建国际化配置文件
messages.properties
DemoData.username=Username DemoData.age=Age
messages_en_US.properties
DemoData.username=Username DemoData.age=Age
messages_zh_CN.properties
DemoData.username=用户名 DemoData.age=年龄
测试类的注解信息上,使用 {} 标记配置文件中的 key
@Data
public class DemoData {
@ExcelProperty(value = "{DemoData.username}", index = 0)
private String username;
@ExcelProperty(value = "{DemoData.age}", index = 1)
private Integer age;
}
**导出注解上设置 i18nHeader=true **
@ResponseExcel(name = "i18nExport", i18nHeader = true)
@GetMapping("excelExport")
public List<DemoData> i18nExport(){
List<DemoData> list=new ArrayList<>();
for(int i=0;i< 10;i++){
DemoData demoData=new DemoData();
demoData.setUsername("username:"+i);
demoData.setAge(i);
list.add(demoData);
}
return list;
}
使用 Postman 测试导出
请求头上使用 Accept-Language 指定当前语言区域,中文是 zh-CN, 英文是 en-US
SpringBoot 的国际化默认会读取请求头中的
Accept-Language进行判断当前区域,可以通过定制LocaleResolver替换这一默认行为

导出效果

导入 controller
注意,这里导入接受的对象如果和导出是同一个的话,由于列名是国际化配置的占位符,无法和实际上传文件进行对应,所以需要给该对象的属性指定 index,导入文件根据 index 进行数据映射。
当然,也可以使用额外的导入类来接收导入信息。
@PostMapping("i18n")
@ResponseBody
public List<DemoData> importExcel(@RequestExcel List<DemoData> list){
return list;
}
/**
* 导入时候回显行号
*/
@ExcelLine
@ExcelIgnore
private Long lineNum;
使用 Postman 测试导入

0.0.7 版本开始添加了全局自定义转换器注入的功能,你只需要将自定义的 Converter 注册成 Spring bean 即可。
示例代码如下(对 set 类型转换):
@Data
public class TestModel {
@ExcelProperty("名称集合")
private Set<String> nameSet;
}
/**
* 集合转换器
*
*/
@Component
public class SetConverter implements Converter<Set<?>> {
private final ConversionService conversionService;
SetConverter() {
this.conversionService = DefaultConversionService.getSharedInstance();
}
@Override
public Class<?> supportJavaTypeKey() {
return Set.class;
}
@Override
public CellDataTypeEnum supportExcelTypeKey() {
return CellDataTypeEnum.STRING;
}
@Override
public Set<?> convertToJavaData(CellData cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
String[] value = StringUtils.delimitedListToStringArray(cellData.getStringValue(), ",");
return (Set<?>) conversionService.convert(value, TypeDescriptor.valueOf(String[].class), new TypeDescriptor(contentProperty.getField()));
}
@Override
public CellData<String> convertToExcelData(Set<?> value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
return new CellData<>(StringUtils.collectionToCommaDelimitedString(value));
}
}
/**
* 默认读取 classpath:excel/ 目录下的模板文件,具体模板使用参考官方文档
*
*/
@ResponseExcel(name = "模板测试excel", sheet = "sheetName", template = "example.xlsx")
@GetMapping("/e1")
public List<DemoData> e1(){
return list();
}
@Documented
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExcelNotation {
/**
* 文本内容
*/
String value() default "";
}
此处只是简单的改变表头字体颜色,醒目提醒用户字段必填
@Documented
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExcelRequired {
/**
* 字体颜色
*/
IndexedColors frontColor() default IndexedColors.RED;
}
用于设置导出时,excel 的单元格下拉框的选项
@Documented
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExcelSelector {
/**
* 固定数据
*/
String[] value() default {};
/**
* 字典key
*/
String dictKeyValue() default "";
/**
* 服务类,需要交给spring管理,如 @Service,如果只有一个实现类时,此属性可以不设置
*/
Class<? extends ExcelSelectorService> serviceClass() default ExcelSelectorService.class;
/**
* 设置下拉框的起始行,默认为表头的下一行
*/
int firstRow() default -1;
/**
* 设置下拉框的结束行,默认为int最大值,65535
*/
int lastRow() default 0x10000;
}
类注解
@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExcelUniqueFields {
/**
* 该组内所有字段组合保证唯一性
*
* @return
*/
String[] groupUnique() default {};
/**
* 该组内每个字段单独保证唯一,字段之间可以允许重复
*
* @return
*/
String[] singleUnique() default {};
/**
* 空值不做校验,false时 null 也会当成值的一种参与校验
*
* @return
*/
boolean skipNull() default true;
}