SpringBoot实现Excel导入导出
在我们平时工作中经常会遇到要操作Excel的功能,比如导出个用户信息或者订单信息的Excel报表。你肯定听说过
POI这个东西,可以实现。但是POI实现的API确实很麻烦,它需要写那种逐行解析的代码(类似Xml解析)。今天
给大家推荐一款非常好用的Excel导入导出工具EasyPoi,希望对大家有所帮助!
1、EasyPoi简介
用惯了SpringBoot的朋友估计会想到,有没有什么办法可以直接定义好需要导出的数据对象,然后添加几个注
解,直接自动实现Excel导入导出功能?
EasyPoi正是这么一款工具,如果你不太熟悉POI,想简单地实现Excel操作,用它就对了!
EasyPoi的目标不是替代POI,而是让一个不懂导入导出的人也能快速使用POI完成Excel的各种操作,而不是看很
多API才可以完成这样的工作。
项目官网:https://gitee.com/lemur/easypoi
2、集成
在SpringBoot中集成EasyPoi非常简单,只需添加easypoi-spring-boot-starter
依赖即可,真正的开箱即
用!
2.1 pom依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.6</version>
<relativePath/>
</parent>
<groupId>com.easypoi</groupId>
<artifactId>spring-boot-easypoi</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-boot-easypoi</name>
<description>pringBoot实现Excel导入导出</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>cn.afterturn</groupId>
<artifactId>easypoi-spring-boot-starter</artifactId>
<version>4.4.0</version>
</dependency>
<!--springfox swagger官方Starter-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>3.0.0</version>
</dependency>
<!--lombok依赖-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--SpringBoot通用依赖模块-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2.2 结果集封装
package com.easypoi.common.api;
/**
* 封装API的错误码
*/
public interface IErrorCode {
long getCode();
String getMessage();
}
package com.easypoi.common.api;
/**
* 枚举了一些常用API操作码
*/
public enum ResultCode implements IErrorCode {
SUCCESS(200, "操作成功"),
FAILED(500, "操作失败"),
VALIDATE_FAILED(404, "参数检验失败"),
UNAUTHORIZED(401, "暂未登录或token已经过期"),
FORBIDDEN(403, "没有相关权限");
private long code;
private String message;
private ResultCode(long code, String message) {
this.code = code;
this.message = message;
}
public long getCode() {
return code;
}
public String getMessage() {
return message;
}
}
package com.easypoi.common.api;
/**
* 通用返回对象
*/
public class CommonResult<T> {
private long code;
private String message;
private T data;
protected CommonResult() {
}
protected CommonResult(long code, String message, T data) {
this.code = code;
this.message = message;
this.data = data;
}
/**
* 成功返回结果
*
* @param data 获取的数据
*/
public static <T> CommonResult<T> success(T data) {
return new CommonResult<T>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), data);
}
/**
* 成功返回结果
*
* @param data 获取的数据
* @param message 提示信息
*/
public static <T> CommonResult<T> success(T data, String message) {
return new CommonResult<T>(ResultCode.SUCCESS.getCode(), message, data);
}
/**
* 失败返回结果
*
* @param errorCode 错误码
*/
public static <T> CommonResult<T> failed(IErrorCode errorCode) {
return new CommonResult<T>(errorCode.getCode(), errorCode.getMessage(), null);
}
/**
* 失败返回结果
*
* @param message 提示信息
*/
public static <T> CommonResult<T> failed(String message) {
return new CommonResult<T>(ResultCode.FAILED.getCode(), message, null);
}
/**
* 失败返回结果
*/
public static <T> CommonResult<T> failed() {
return failed(ResultCode.FAILED);
}
/**
* 参数验证失败返回结果
*/
public static <T> CommonResult<T> validateFailed() {
return failed(ResultCode.VALIDATE_FAILED);
}
/**
* 参数验证失败返回结果
*
* @param message 提示信息
*/
public static <T> CommonResult<T> validateFailed(String message) {
return new CommonResult<T>(ResultCode.VALIDATE_FAILED.getCode(), message, null);
}
/**
* 未登录返回结果
*/
public static <T> CommonResult<T> unauthorized(T data) {
return new CommonResult<T>(ResultCode.UNAUTHORIZED.getCode(), ResultCode.UNAUTHORIZED.getMessage(), data);
}
/**
* 未授权返回结果
*/
public static <T> CommonResult<T> forbidden(T data) {
return new CommonResult<T>(ResultCode.FORBIDDEN.getCode(), ResultCode.FORBIDDEN.getMessage(), data);
}
public long getCode() {
return code;
}
public void setCode(long code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
2.3 Swagger配置
package com.easypoi.config;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
/**
* Swagger2API文档的配置
*/
@Configuration
@EnableSwagger2
public class Swagger2Config {
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
//为当前包下controller生成API文档
.apis(RequestHandlerSelectors.basePackage("com.easypoi.controller"))
//为有@Api注解的Controller生成API文档
.apis(RequestHandlerSelectors.withClassAnnotation(Api.class))
//为有@ApiOperation注解的方法生成API文档
.apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
.paths(PathSelectors.any())
.build();
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("SwaggerUI演示")
.description("mall-tiny")
.contact(new Contact("macro", null, null))
.version("1.0")
.build();
}
}
2.4 启动类
package com.easypoi;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SpringBootEasypoiApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootEasypoiApplication.class, args);
}
}
2.5 配置文件
server:
port: 8088
springfox:
documentation:
enabled: true
2.6 实体类
package com.easypoi.domain;
import cn.afterturn.easypoi.excel.annotation.Excel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.Date;
/**
* 购物会员
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class Member {
@Excel(name = "ID", width = 10)
private Long id;
@Excel(name = "用户名", width = 20, needMerge = true)
private String username;
private String password;
@Excel(name = "昵称", width = 20, needMerge = true)
private String nickname;
@Excel(name = "出生日期", width = 20, format = "yyyy-MM-dd")
private Date birthday;
@Excel(name = "手机号", width = 20, needMerge = true, desensitizationRule = "3_4")
private String phone;
private String icon;
@Excel(name = "性别", width = 10, replace = {"男_0", "女_1"})
private Integer gender;
}
package com.easypoi.domain;
import cn.afterturn.easypoi.excel.annotation.Excel;
import cn.afterturn.easypoi.excel.annotation.ExcelCollection;
import cn.afterturn.easypoi.excel.annotation.ExcelEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.Date;
import java.util.List;
/**
* 订单
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class Order {
@Excel(name = "ID", width = 10, needMerge = true)
private Long id;
@Excel(name = "订单号", width = 20, needMerge = true)
private String orderSn;
@Excel(name = "创建时间", width = 20, format = "yyyy-MM-dd HH:mm:ss", needMerge = true)
private Date createTime;
@Excel(name = "收货地址", width = 20, needMerge = true)
private String receiverAddress;
@ExcelEntity(name = "会员信息")
private Member member;
@ExcelCollection(name = "商品列表")
private List<Product> productList;
}
package com.easypoi.domain;
import cn.afterturn.easypoi.excel.annotation.Excel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
/**
* 商品
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class Product {
@Excel(name = "ID", width = 10)
private Long id;
@Excel(name = "商品SN", width = 20)
private String productSn;
@Excel(name = "商品名称", width = 20)
private String name;
@Excel(name = "商品副标题", width = 30)
private String subTitle;
@Excel(name = "品牌名称", width = 20)
private String brandName;
@Excel(name = "商品价格", width = 10)
private BigDecimal price;
@Excel(name = "购买数量", width = 10, suffix = "件")
private Integer count;
}
2.7 工具类
package com.easypoi.util;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.io.resource.ClassPathResource;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONUtil;
import java.nio.charset.Charset;
import java.util.List;
/**
* 从本地获取JSON数据的工具类
*/
public class LocalJsonUtil {
/**
* 从指定路径获取JSON并转换为List
*
* @param path json文件路径
* @param elementType List元素类型
*/
public static <T> List<T> getListFromJson(String path, Class<T> elementType) {
ClassPathResource resource = new ClassPathResource(path);
String jsonStr = IoUtil.read(resource.getStream(), Charset.forName("UTF-8"));
JSONArray jsonArray = new JSONArray(jsonStr);
return JSONUtil.toList(jsonArray, elementType);
}
}
LocalJsonUtil工具类,可以直接从resources目录下获取JSON数据并转化为对象。
2.8 字段处理器
package com.easypoi.handler;
import cn.afterturn.easypoi.handler.impl.ExcelDataHandlerDefaultImpl;
import cn.hutool.core.util.StrUtil;
import com.easypoi.domain.Member;
/**
* 自定义字段处理
*/
public class MemberExcelDataHandler extends ExcelDataHandlerDefaultImpl<Member> {
@Override
public Object exportHandler(Member obj, String name, Object value) {
if("昵称".equals(name)){
String emptyValue = "暂未设置";
if(value==null){
return super.exportHandler(obj,name,emptyValue);
}
if(value instanceof String&&StrUtil.isBlank((String) value)){
return super.exportHandler(obj,name,emptyValue);
}
}
return super.exportHandler(obj, name, value);
}
@Override
public Object importHandler(Member obj, String name, Object value) {
return super.importHandler(obj, name, value);
}
}
2.9 处理器
package com.easypoi.controller;
import cn.afterturn.easypoi.entity.vo.NormalExcelConstants;
import cn.afterturn.easypoi.excel.ExcelImportUtil;
import cn.afterturn.easypoi.excel.entity.ExportParams;
import cn.afterturn.easypoi.excel.entity.ImportParams;
import cn.afterturn.easypoi.excel.entity.enmus.ExcelType;
import cn.afterturn.easypoi.view.PoiBaseView;
import com.easypoi.common.api.CommonResult;
import com.easypoi.domain.Member;
import com.easypoi.domain.Order;
import com.easypoi.domain.Product;
import com.easypoi.handler.MemberExcelDataHandler;
import com.easypoi.util.LocalJsonUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
/**
* EasyPoi导入导出测试Controller
*/
@Controller
@Api(tags = "EasyPoiController", description = "EasyPoi导入导出测试")
@RequestMapping("/easyPoi")
public class EasyPoiController {
@ApiOperation(value = "导出会员列表Excel")
@RequestMapping(value = "/exportMemberList", method = RequestMethod.GET)
public void exportMemberList(ModelMap map,
HttpServletRequest request,
HttpServletResponse response) {
List<Member> memberList = LocalJsonUtil.getListFromJson("json/members.json", Member.class);
ExportParams params = new ExportParams("会员列表", "会员列表", ExcelType.XSSF);
//对导出结果进行自定义处理
MemberExcelDataHandler handler = new MemberExcelDataHandler();
handler.setNeedHandlerFields(new String[]{"昵称"});
params.setDataHandler(handler);
map.put(NormalExcelConstants.DATA_LIST, memberList);
map.put(NormalExcelConstants.CLASS, Member.class);
map.put(NormalExcelConstants.PARAMS, params);
map.put(NormalExcelConstants.FILE_NAME, "memberList");
PoiBaseView.render(map, request, response, NormalExcelConstants.EASYPOI_EXCEL_VIEW);
}
@ApiOperation("从Excel导入会员列表")
@RequestMapping(value = "/importMemberList", method = RequestMethod.POST)
@ResponseBody
public CommonResult importMemberList(@RequestPart("file") MultipartFile file) {
ImportParams params = new ImportParams();
params.setTitleRows(1);
params.setHeadRows(1);
try {
List<Member> list = ExcelImportUtil.importExcel(
file.getInputStream(),
Member.class, params);
return CommonResult.success(list);
} catch (Exception e) {
e.printStackTrace();
return CommonResult.failed("导入失败!");
}
}
@ApiOperation(value = "导出订单列表Excel")
@RequestMapping(value = "/exportOrderList", method = RequestMethod.GET)
public void exportOrderList(ModelMap map,
HttpServletRequest request,
HttpServletResponse response) {
List<Order> orderList = getOrderList();
ExportParams params = new ExportParams("订单列表", "订单列表", ExcelType.XSSF);
//导出时排除一些字段
params.setExclusions(new String[]{"ID", "出生日期", "性别"});
map.put(NormalExcelConstants.DATA_LIST, orderList);
map.put(NormalExcelConstants.CLASS, Order.class);
map.put(NormalExcelConstants.PARAMS, params);
map.put(NormalExcelConstants.FILE_NAME, "orderList");
PoiBaseView.render(map, request, response, NormalExcelConstants.EASYPOI_EXCEL_VIEW);
}
private List<Order> getOrderList() {
List<Order> orderList = LocalJsonUtil.getListFromJson("json/orders.json", Order.class);
List<Product> productList = LocalJsonUtil.getListFromJson("json/products.json", Product.class);
List<Member> memberList = LocalJsonUtil.getListFromJson("json/members.json", Member.class);
for (int i = 0; i < orderList.size(); i++) {
Order order = orderList.get(i);
order.setMember(memberList.get(i));
order.setProductList(productList);
}
return orderList;
}
}
2.10 测试
接下来介绍下EasyPoi的使用,以会员信息和订单信息的导入导出为例,分别实现下简单的单表导出和具有关联信
息的复杂导出。
2.10.1 简单导出
我们以会员信息列表导出为例,使用EasyPoi来实现下导出功能,看看是不是够简单!
-
首先创建一个会员对象
Member
,封装会员信息; -
在此我们就可以看到EasyPoi的核心注解
@Excel
,通过在对象上添加@Excel
注解,可以将对象信息直接导出到Excel中去,下面对注解中的属性做个介绍;
-
-
name:Excel中的列名;
-
width:指定列的宽度;
-
needMerge:是否需要纵向合并单元格;
-
format:当属性为时间类型时,设置时间的导出导出格式;
-
desensitizationRule:数据脱敏处理,
3_4
表示只显示字符串的前3
位和后4
位,其他为*
号; -
replace:对属性进行替换;
-
suffix:对数据添加后缀。
-
-
接下来我们在Controller中添加一个接口,用于导出会员列表到Excel,导出的
member.json
的内容:
[
{
"id": 1,
"username": "admin",
"password": null,
"nickname": "Admin",
"birthday": "1994-12-31",
"phone": "18790000000",
"icon": null,
"gender": 0
},
{
"id": 2,
"username": "macro",
"password": null,
"nickname": "Macro",
"birthday": "1995-01-31",
"phone": "18791000000",
"icon": null,
"gender": 0
},
{
"id": 3,
"username": "andy",
"password": null,
"nickname": "Andy",
"birthday": "1995-02-28",
"phone": "18792000000",
"icon": null,
"gender": 1
},
{
"id": 4,
"username": "ruby",
"password": null,
"nickname": "Ruby",
"birthday": "1995-03-31",
"phone": "18793000000",
"icon": null,
"gender": 1
},
{
"id": 5,
"username": "tom",
"password": null,
"nickname": "",
"birthday": "1995-03-31",
"phone": "18793000000",
"icon": null,
"gender": 1
}
]
运行项目,直接通过Swagger访问接口,注意在Swagger中访问接口无法直接下载,需要点击返回结果中的下载按
钮才行,访问地址:http://localhost:8088/swagger-ui/
下载完成后,查看下文件,一个标准的Excel文件已经被导出了。
memberList.xlsx
文件内容:
2.10.2 简单导入
导入功能实现起来也非常简单,下面以会员信息列表的导入为例。
-
在Controller中添加会员信息导入的接口,这里需要注意的是使用
@RequestPart
注解修饰文件上传参数,否则在Swagger中就没法显示上传按钮了;
-
然后在Swagger中测试接口,选择之前导出的Excel文件即可,导入成功后会返回解析到的数据。
得到的结果:
{
"code": 200,
"message": "操作成功",
"data": [
{
"id": 1,
"username": "admin",
"password": null,
"nickname": "Admin",
"birthday": "1994-12-30T16:00:00.000+00:00",
"phone": "187****0000",
"icon": null,
"gender": 0
},
{
"id": 2,
"username": "macro",
"password": null,
"nickname": "Macro",
"birthday": "1995-01-30T16:00:00.000+00:00",
"phone": "187****0000",
"icon": null,
"gender": 0
},
{
"id": 3,
"username": "andy",
"password": null,
"nickname": "Andy",
"birthday": "1995-02-27T16:00:00.000+00:00",
"phone": "187****0000",
"icon": null,
"gender": 1
},
{
"id": 4,
"username": "ruby",
"password": null,
"nickname": "Ruby",
"birthday": "1995-03-30T16:00:00.000+00:00",
"phone": "187****0000",
"icon": null,
"gender": 1
},
{
"id": 5,
"username": "tom",
"password": null,
"nickname": "暂未设置",
"birthday": "1995-03-30T16:00:00.000+00:00",
"phone": "187****0000",
"icon": null,
"gender": 1
}
]
}
2.10.3 复杂导出
当然EasyPoi也可以实现更加复杂的Excel操作,比如导出一个嵌套了会员信息和商品信息的订单列表,下面我们来
实现下!
-
首先添加商品对象
Product
,用于封装商品信息; -
然后添加订单对象
Order
,订单和会员是一对一关系,使用@ExcelEntity
注解表示,订单和商品是一对多关系,使用
@ExcelCollection
注解表示,Order
就是我们需要导出的嵌套订单数据; -
接下来在Controller中添加导出订单列表的接口,由于有些会员信息我们不需要导出,可以调用
ExportParams
中的setExclusions
方法排除掉; -
在Swagger中访问接口测试,导出订单列表对应Excel;
products.json
的内容:
[
{
"id": 1,
"productSn": "7437788",
"name": "小米8",
"subTitle": "全面屏游戏智能手机 6GB+64GB 黑色 全网通4G 双卡双待",
"brandName": "小米",
"price": 2699,
"count": 1
},
{
"id": 2,
"productSn": "7437789",
"name": "红米5A",
"subTitle": "全网通版 3GB+32GB 香槟金 移动联通电信4G手机 双卡双待",
"brandName": "小米",
"price": 649,
"count": 1
},
{
"id": 3,
"productSn": "7437799",
"name": "Apple iPhone 8 Plus",
"subTitle": "64GB 红色特别版 移动联通电信4G手机",
"brandName": "苹果",
"price": 5499,
"count": 1
}
]
orders.json
的内容:
[
{
"id": 1,
"orderSn": "201809150101000001",
"createTime": "2021-10-10 17:02:28",
"receiverAddress": "广东省深圳市"
},
{
"id": 1,
"orderSn": "201809150101000002",
"createTime": "2021-10-11 17:02:28",
"receiverAddress": "江苏省南京市"
},
{
"id": 1,
"orderSn": "201809150101000003",
"createTime": "2021-10-12 17:02:28",
"receiverAddress": "江苏省苏州市"
}
]
- 下载完成后,查看下文件,EasyPoi导出复杂的Excel也是很简单的!
2.10.4 自定义处理
如果你想对导出字段进行一些自定义处理,EasyPoi也是支持的,比如在会员信息中,如果用户没有设置昵称,我
们添加下暂未设置
信息。
-
我们需要添加一个处理器继承默认的
ExcelDataHandlerDefaultImpl
类,然后在exportHandler
方法中实现自定义处理逻辑;
-
然后修改Controller中的接口,调用
MemberExcelDataHandler
处理器的setNeedHandlerFields
设置需要自定义处理的字段,并调用
ExportParams
的setDataHandler
设置自定义处理器; -
再次调用导出接口,我们可以发现昵称已经添加默认设置了。
上面的代码已经是自定义处理过的。
//对导出结果进行自定义处理
MemberExcelDataHandler handler = new MemberExcelDataHandler();
handler.setNeedHandlerFields(new String[]{"昵称"});
params.setDataHandler(handler);
3、总结
体验了一波EasyPoi,它使用注解来操作Excel的方式确实非常好用。如果你想生成更为复杂的Excel的话,可以考
虑下它的模板功能。