目录💻
- 一、介绍
- 需求背景
- 二、实现步骤
- 1、依赖
- 1.1、Maven
- 1.2、Gradle
- 2、实体类
- 2.1、学生分数表
- 2.2、Sheet 工作表对象
- 3、excel工具类
- 4、Service层
- 接口
- 实现类
- 5、Controller层
- 三、测试效果
)
一、介绍
EasyExcel是阿里巴巴开源的一个基于Java的、快速、简洁、解决大文件内存溢出的Excel处理工具。他能让你在不用考虑性能、内存的等因素的情况下,快速完成Excel的读、写等功能。
需求背景
最近我们公司需要实现报表的导出功能需要实现每个产品根据用户选择的时间导出不同长度的时间效果,所以再导出的时候除了需要固定的每个产品的列,还需要根据选择的时间导出不确定时间的列。下面是我根据公司的产品导出报表模拟的数据导出的效果图!
先看看需要实现的效果:
- 创建了多个sheet,这个地方我共用的一个header表头
- header表头实现合并
- 内容实现合并单元格
- 根据内容实现不定长Excel表头和内容导出
- sheet页面实现和sheet共用一个表头,但内容单独写入
数据库的内容原始格式(下面有实体类)
二、实现步骤
代码基本上实现了把功能代码都拆分出来了,只需要根据自己的业务情况实现部分修改即可套用
1、依赖
1.1、Maven
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.1.1</version>
</dependency>
1.2、Gradle
implementation("com.alibaba:easyexcel:3.1.1")
2、实体类
2.1、学生分数表
项目业务实际用到的表
/**
* 学生分数表
*/
@Data
public class Score {
/**姓名*/
private String name;
/**期中语文分数*/
private Integer monthLanguage;
/**期中数学分数*/
private Integer monthMathematics;
/**期中英语分数*/
private Integer monthEnglish;
/**期末语文分数*/
private Integer finalLanguage;
/**期末数学分数*/
private Integer finalMathematics;
/**期末英语分数*/
private Integer finalEnglish;
/**学期*/
private String semester;
}
2.2、Sheet 工作表对象
一个工作表一个该对象,主要用来拆分每个Sheet 的表头数据,名称会不一样,
我们再实现导出的时候表头和内容的长度需要一致,这样才可以去对应上每一个单元格
一个List<List<String>>
里面的一个list
代表一行数据,String
代表每一个单元格的数据
@Data
public class Sheet {
/** 文件名称 */
String name;
/** 表头列表 */
List<List<String>> headerList;
/** 内容列表 */
List<List<String>> dataList;
/** 单元格合并行数 */
Integer eachRow;
}
3、excel工具类
该类主要是把导出的单独拆分出来,并且拆分出两个方法,一个是处理response。 一个处理ExcelWriter
客户端对象,然后通过我们传入的List<Sheet>
去单独处理自己的WriteSheet
对象参数,我这里因为不同的sheet只有合并的单元格不同,和名字不同,如果自己有其他的,也可以往Sheet
对象里添加属性去用于单独配置
public class ExcelUtil {
/**
* 导出Excel
*
* @param response
* @param name
* @param list
*/
public static void exportExcel(
HttpServletResponse response, String name, List<Sheet> list) {
try {
setExcelResponseProp(response, name);
ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream()).build();
// 定义列头的样式:居中对齐
WriteCellStyle headWriteCellStyle = new WriteCellStyle();
headWriteCellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
headWriteCellStyle.setVerticalAlignment(VerticalAlignment.CENTER);
// 定义内容的样式:居中对齐
WriteCellStyle contentWriteCellStyle = new WriteCellStyle();
contentWriteCellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
contentWriteCellStyle.setVerticalAlignment(VerticalAlignment.CENTER);
for (Sheet sheet : list) {
WriteSheet writeSheet =
EasyExcel.writerSheet(sheet.getName())
.head(sheet.getHeaderList())
.registerWriteHandler(new LoopMergeStrategy(sheet.getEachRow(), 0)) // 设置第一列每3行合并
.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) // 自定义行高
.registerWriteHandler(new HorizontalCellStyleStrategy(headWriteCellStyle, contentWriteCellStyle)) //居中对其
.build();
excelWriter.write(sheet.getDataList(), writeSheet);
}
// 注意关闭excelWriter,释放资源
excelWriter.finish();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* 设置响应结果
*
* @param response 响应结果对象
* @param rawFileName 文件名
* @throws UnsupportedEncodingException 不支持编码异常
*/
private static void setExcelResponseProp(HttpServletResponse response, String rawFileName)
throws UnsupportedEncodingException {
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
String fileName = java.net.URLEncoder.encode(rawFileName, "UTF-8");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
}
}
4、Service层
接口
public interface ScoreService {
List<Sheet> exportExcel();
}
实现类
该实现类是重点,实现怎么把数据库类转为对应的用于写入sheet的集合的
步骤解析
- 先转换可变从表头的数据,总共多少行表头: 第1学期…
- 添加header列表:添加固定的表头和可变长的,如果是单表头,只需要写一个到集合就行,如果是合并的二级表头,则添加多个,内部会按照下标去取,如果相同的会自动合并
格式会是下面这样- 添加sheet1内容:取出来的内容会是数据库按照学生每次考试成绩来存放的
得到数据后先按照学生分组放入map
再根据学生去循环取出每次考试的成绩
引用了getSubjects方法,因为我们数据是不同科目存放到一行数据的,现在做展现需要把一行数据的不同学科加到不同的行去。需要需要循环,去取不同的字段,这里通过了转map再通过拼接字段名去取
这个地方需要注意的是第一次循环肯定要是用作放入每行数据的,我们添加数据的时候需要以行来作为一个集合添加数据
展现的数据
一条数据长度需要对应表头的长度,因为底层是通过下标去写入到每个单元格
- 添加sheet2内容:和1一样的,也是添加数据,最终把数据添加到集合,长度也需要和对应的表头一致,一一对应上。
- 把两个sheet的数据添加到集合返回,确认自己每个合并的单元格是多少,这个地方合并的需要一致
- 完整代码
@Service
public class ScoreServiceImpl implements ScoreService {
@Override
public List<Sheet> exportExcel() {
List<Sheet> sheetList = new ArrayList<>();
// 1、先转换可变从表头的数据,总共多少行表头: 第1学期.....
// 获取统计日期(可变长字段)
List<String> dateList = new ArrayList<>();
for (int i = 1; i <= 6; i++) {
String date = String.format("第%s学期", i);
dateList.add(date);
}
// 2、添加header列表
// 添加表头列表
// 固定表头
List<List<String>> headerList = new ArrayList<>();
headerList.add(new ArrayList<>(Arrays.asList("姓名")));
headerList.add(new ArrayList<>(Arrays.asList("科目")));
// 可变长的表头,通过循环添加到集合中,如果是拆分的二级表头,则需要再集合中添加多个数据 Arrays.asList(date, "期中考试")
dateList.forEach(date -> {
headerList.add(new ArrayList<>(Arrays.asList(date, "期中考试")));
headerList.add(new ArrayList<>(Arrays.asList(date, "期末考试")));
});
// 3. 添加sheet1内容:取出来的内容会是数据库按照学生每次考试成绩来存放的
//数据列表
List<Score> list = getData();
Map<String, List<Score>> listMap = //以姓名分组
list.stream().collect(Collectors.groupingBy(Score::getName));
List<List<String>> sheetDate = new ArrayList<>();
for (Map.Entry<String, List<Score>> e : listMap.entrySet()) {
String trolleyName = e.getKey();
List<Score> v = e.getValue();
for (Map.Entry<String, String> entry : getSubjects().entrySet()) { //循环科目,看总共有多少个科目
// 添加姓名和科目
List<String> dataList = new ArrayList<>();
dataList.add(trolleyName);
String key = entry.getKey();
String value = entry.getValue();
dataList.add(value);
// 添加每次学期数据
for (int i = 0; i < dateList.size(); i++) {
String startDate = dateList.get(i);
//做排序,以总分成绩做key,数据做value
Map<String, Score> dayMap =
v.stream()
.collect(
Collectors.toMap(
k-> dateList.get(Integer.parseInt(k.getSemester())-1),
p -> p,
(s, a) -> a));
String month = "";
String finals = "";
if (dayMap.containsKey(startDate)) { //判断,如果没数据,则添加空数据,避免后面的数据写错位
Score item = dayMap.get(startDate);
Map<String, Object> trolleyMap = BeanUtil.beanToMap(item); //通过名称去循环写入
month = trolleyMap.get("month" + key).toString();
finals = trolleyMap.get("final" + key).toString();
}
dataList.add(month);
dataList.add(finals);
}
sheetDate.add(dataList);
}
}
// 4. 添加sheet2内容
// 取出每个学期的数据去做分组
Map<String, List<Score>> dayMap =
list.stream()
.collect(
Collectors.groupingBy(
k-> dateList.get(Integer.parseInt(k.getSemester())-1)));
List<List<String>> sheetDate2 = new ArrayList<>(); //一个sheet页面
for (int i = 1; i <= 10; i++) { //循环10次,取前10名
List<String> lineList = new ArrayList<>(); //一行数据
lineList.add("总分排名");
lineList.add(String.valueOf(i));
for (String date : dateList) { //每个学期
String month = "", finals = "";
if (dayMap.containsKey(date)) { //每次考试
List<Score> monthList = dayMap.get(date).stream()
.sorted(Comparator.comparing(p -> p.getMonthEnglish() + p.getMonthLanguage() + p.getMonthMathematics()))
.toList();
if (monthList.size() >= i) {
Score score = monthList.get(monthList.size() - i);
month = score.getName() + ":" +
(score.getMonthEnglish() + score.getMonthLanguage() + score.getMonthMathematics());
}
List<Score> finalList =
dayMap.get(date).stream()
.sorted(Comparator.comparing(p -> p.getFinalEnglish() + p.getFinalLanguage() + p.getFinalMathematics()))
.toList();
if (finalList.size() >= i) {
Score score = finalList.get(monthList.size() - i);
finals = score.getName() + ":"
+ (score.getFinalEnglish() + score.getFinalLanguage() + score.getFinalMathematics());
}
}
lineList.add(month);
lineList.add(finals);
}
sheetDate2.add(lineList);
}
// 5. 把两个sheet的数据添加到list
Sheet sheet = new Sheet();
sheet.setName("学生成绩统计");
sheet.setHeaderList(headerList);
sheet.setDataList(sheetDate);
sheet.setEachRow(3); //合并单元格,合并的单元格内容要一样
sheetList.add(sheet);
Sheet sheet2 = new Sheet();
sheet2.setName("成绩总分排名");
sheet2.setHeaderList(headerList);
sheet2.setDataList(sheetDate2);
sheet2.setEachRow(10);
sheetList.add(sheet2);
return sheetList;
}
/**
* 科目
*/
private Map<String, String> getSubjects() {
final Map<String, String> map = new HashMap<>();
map.put("Language", "语文");
map.put("Mathematics", "数学");
map.put("English", "英语");
return map;
}
/**
* 模拟数据库取出数据
* @return
*/
private static List<Score> getData(){
List<Score> list = new ArrayList<>();
final String[] surnames = {"赵伟", "钱伟", "孙勇", "李芳", "周娜", "郑敏", "王敏", "冯静", "南宫伟","孙岚"};
for (int i = 1; i <= 6; i++) {
for (int j = 0; j < surnames.length; j++) {
Score score = new Score();
score.setName(surnames[j]);
score.setFinalEnglish(new Random().nextInt(101) );
score.setFinalLanguage(new Random().nextInt(101) );
score.setFinalMathematics(new Random().nextInt(101) );
score.setMonthEnglish(new Random().nextInt(101) );
score.setMonthLanguage(new Random().nextInt(101) );
score.setMonthMathematics(new Random().nextInt(101) );
score.setSemester(String.valueOf(i));
list.add(score);
}
}
return list;
}
}
5、Controller层
@RestController
@RequestMapping("excel")
public class ScoreController {
@Resource
private ScoreService scoreService;
@GetMapping("/exportExcel")
public void exportExcel(HttpServletResponse response) {
List<Sheet> sheets = scoreService.exportExcel();
//传入响应,返回的下载文件名称,以及页面内容
ExcelUtil.exportExcel(response, "学生成绩汇总", sheets);
}
}
三、测试效果
-
最终导出文件效果
-
sheet对象内容