辛苦写的,转载请注明来源^_^
一、为什么要写这个Excel工具类
上个项目有个功能点需要导出信息到Excel文件,于是到网上找了工具类xdemo,首先感谢原作者的奉献,使用很简单。但在使用的过程中也发现了几个问题(可能是我用的版本太老导致的):
1.类的属性为空时,没有相应处理,Excel中的单元格会前移一个单元格。
2.不支持Java8的LocalDateTime的日期转换。
3.Excel反序列化List时,不支持自定义日期格式。
4.由于写的时间较早,其中很多POI的API都是标记过时的。
5.代码中大量使用for循环,if判断,对于代码洁癖的我,有点不习惯(对原作者没有任何恶意^_^,不要误解)
二、和老版的Excel工具类对比
功能点 | 新版ExcelUtils | 旧版ExcelUtils |
---|---|---|
自定义表头样式 | 支持 | 不支持 |
自定义内容样式 | 支持 | 不支持 |
自定义序列化属性转换 | 支持 | 支持 |
自定义反序列属性转换 | 支持 | 支持 |
支持LocalDateTime | 支持 | 不支持 |
自定义表格合并 | 不支持(写出来太费劲^_^) | 支持 |
三、使用的依赖
Gradle
//FastJson
compile ("com.alibaba:fastjson:1.2.35")
//POI
compile ("org.apache.poi:poi:3.16")
compile ("org.apache.poi:poi-ooxml:3.16")
Maven
//FastJson
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.35</version>
</dependency>
//POI
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>3.16</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>3.16</version>
</dependency>
四、使用的技术
1.使用FastJson作为Json转换工具(号称最快的Json解析工具)
2.大量使用Java8的Stream进行链式操作(好像也写了一些for循环……)
3.利用Java8的lambda表达式简化语法
4.利用Java8的Optional排除空指针异常
工具中采用的思想是约定大于配置。必需按照约定好的步骤进行调用。
五、List转Excel的实现思想
1.顺序获取类中所有属性的Excel注解,拿到Excel注解的name属性,设置为表头。
2.逐个顺序获取对象中带有Excel注解的属性值,然后做对应的转换(false->假,true->真等),之后写入到cell中,cell的类型均为String类型。
3.写出文件,搞定了
六、Excel转List的实现思想
可不可以把每行转换成一个对象的Json格式,再用Json工具转换成对象呢?这样效率可能会慢一点,但事实证明并没有慢多少,而且很方便》》》》
1.读取类中的所有带有Excel注解的属性的属性名(个数应和Exel的列的个数对应)。
2.将每一行组成Map<String,String>
的数据,代表一个对象。那么多选就是List<Map<String,String>>
,然后将其序列化,就类似于这样的Json:
[
{
"name":"king1",
"age":"18"
},
{
"name":"king2",
"age":"19"
},
......
]
组合成了对象数组的Json格式了,这时候只要调用Json工具的List list = JSON.parseArray(json, Class clazz)方法就可以拿到反序列的对象了,这个做法是不是有点骚气==》==》》》》》》
约定:
List中的类中带有Excel注解的属性必需与Excel文件中的数据完全对应,才能反序列成功。说的简单点,就是反序列时类属性上的@Excel注解要和导出为Excel时加的@Excel注解一样。
七、Excel工具类代码
7.1 Excel.java
package com.kingboy.common.utils.excel;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author kingboy--KingBoyWorld@163.com
* @date 2017/7/26 下午1:23
* @desc Excel注解,用以生成Excel表格文件.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.TYPE})
public @interface Excel {
//列名
String name() default "";
//宽度
int width() default 25;
//忽略该字段
boolean skip() default false;
//日期格式
String dateFormat() default "yyyy年MM月dd日 HH:mm";
}
7.2 ExcelStyle.java
package com.kingboy.common.utils.excel;
import org.apache.poi.ss.usermodel.BorderStyle;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
/**
* @author kingboy--KingBoyWorld@163.com
* @date 2017/7/26 下午4:58
* @desc 设置Excel的样式.使用了build模式,链式赋值
*/
public class ExcelStyle {
public ExcelStyle() { }
//对齐方式
private HorizontalAlignment align;
//字体 "黑体"等
private String fontName;
//是否加粗,对表头不生效
private boolean isBold;
//字体大小
private short size;
//字体颜色
private short fontColor;
//边框粗细
private BorderStyle borderStyle;
//背景颜色
private short backColor;
public ExcelStyle setAlign(HorizontalAlignment align) {
this.align = align;
return this;
}
public ExcelStyle setFontName(String fontName) {
this.fontName = fontName;
return this;
}
public ExcelStyle setBold(boolean bold) {
isBold = bold;
return this;
}
public ExcelStyle setSize(short size) {
this.size = size;
return this;
}
public ExcelStyle setFontColor(short fontColor) {
this.fontColor = fontColor;
return this;
}
public ExcelStyle setBorderStyle(BorderStyle borderStyle) {
this.borderStyle = borderStyle;
return this;
}
public ExcelStyle setBackColor(short backColor) {
this.backColor = backColor;
return this;
}
public HorizontalAlignment getAlign() {
return align;
}
public String getFontName() {
return fontName;
}
public boolean isBold() {
return isBold;
}
public short getSize() {
return size;
}
public short getFontColor() {
return fontColor;
}
public BorderStyle getBorderStyle() {
return borderStyle;
}
public short getBackColor() {
return backColor;
}
}
7.3 ExcelUtils.java
package com.kingboy.common.utils.excel;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.apache.poi.hssf.util.HSSFColor;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Path;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* @author kingboy--KingBoyWorld@163.com
* @date 2017/7/26 下午1:25
* @desc Excel工具类. 只针对单个类内的属性进行转换。
* 额外说明:本工具类支持设置数据行的格式,但由于这个功能占用执行时间过多(大约十倍),所以在205行注释掉了,有需要可以打开
* 使用说明-博客:http://blog.youkuaiyun.com/kingboyworld/article/details/76253785
*/
public final class ExcelUtils {
private ExcelUtils() { }
/**
* 将List转换成Excel
* @param list 数据集合
* @param filePath java7中的文件操作 创建方式:Paths.get("文件地址");
* @param <T> bean类型
* @return
* @throws Exception
*/
public static <T> void listToExcel(List<T> list, Path filePath, Map<String, Map<String, String>> fieldMapper,
ExcelStyle headStyle, ExcelStyle contentStyle) throws Exception {
//List为null或者empty抛出异常
Optional.ofNullable(list)
.filter(l -> !l.isEmpty())
.orElseThrow(NullPointerException::new);
//1. 获取类的字符串属性
List<Excel> excelList = getExcelAnnoList(list.get(0).getClass());
//2. 创建工件薄,工件表
Workbook workbook = new SXSSFWorkbook();
Sheet sheet = workbook.createSheet();
//3. 设置表头
setHead(excelList, sheet, headStyle);
//4. 设置内容
setContent(list, sheet, fieldMapper, contentStyle);
//5. 写文件
workbook.write(Files.newOutputStream(filePath));
}
//每次转换多少行
private static Integer size = 1_000;
/**
* excel转换为List,基本思路是拼接成Json,然后用Json工具转换为List
* 必需保证类里面的带有Excel注解属性的顺序和Excel文件中标题的顺序相对应
* @param filePath 文件路径 创建方式:Paths.get("文件地址");
* @param clazz 类
* @param <T>
* @return
*/
public static<T> List<T> excelToList(Path filePath, Class<T> clazz, Map<String, Map<String, String>> fieldMapper)
throws Exception {
Workbook workbook = new XSSFWorkbook(Files.newInputStream(filePath));
Sheet sheet = workbook.getSheetAt(0);
//1. 获取类和Excel表格对应的属性,有序放入ArrayList中
ArrayList<String> fieldList = getFieldNameByExcelAnno(clazz);
//2. 获取结果
ArrayList<T> resultList = getClazzArrayList(sheet, clazz, fieldList, fieldMapper);
return resultList;
}
/*------------------------------ExcelToList的调用方法------------------------------------*/
/**
*
* @param sheet 工作表
* @param fieldList 获取类中有映射关系的属性名
* @param <T>
* @return
*/
private static <T> ArrayList<T> getClazzArrayList(Sheet sheet, Class<T> clazz, ArrayList<String> fieldList,
Map<String, Map<String, String>> fieldMapper) {
//数据行数
int rowNumber = sheet.getPhysicalNumberOfRows() - 1;
//读取次数
int times = rowNumber / size + 1;
//结果集
ArrayList<T> result = new ArrayList<T>(rowNumber);
Stream.iterate(1, i -> i + 1)
.limit(times - 1)
.forEach(i -> {
result.addAll(getArrayListFromMap(
readExcel((i - 1) * size + 1, i == times ? rowNumber : size, sheet, fieldList),
clazz,
fieldMapper)
);
});
return result;
}
/**
* 解析Json字符串
* @param mapList Excel表中的数据
* @param clazz
* @param fieldMapper 属性转换容器
* @param <T>
* @return
*/
private static <T> List<T> getArrayListFromMap(List<Map<String, String>> mapList, Class<T> clazz,
Map<String, Map<String, String>> fieldMapper) {
//记录默认的日期格式
String tempDateFormat = JSONObject.DEFFAULT_DATE_FORMAT;
//设置日期编码
getFieldWithExcel(clazz)
.filter(field -> field.getType() == Date.class || field.getType() == LocalDateTime.class)
.findFirst()
.ifPresent(field ->
JSONObject.DEFFAULT_DATE_FORMAT = field.getAnnotation(Excel.class).dateFormat());
//转换属性
if (fieldMapper != null) {
mapList.stream().forEach(map -> {
map.forEach((key, value) -> {
if (fieldMapper.get(key) != null) {
//真实的属性值
String realValue = fieldMapper.get(key).get(value);
//如果有对应的值,就设置对应的值
map.put(key, realValue == null ? value : realValue);
}
});
});
}
List<T> list = JSON.parseArray(JSON.toJSONString(mapList), clazz);
//还原默认的日期格式
JSONObject.DEFFAULT_DATE_FORMAT = tempDateFormat;
return list;
}
/**
* 读取指定数量的数据,每行都拼接成一个Map<String, String>的集合
* @param start 开始行
* @param size 长度
* @param sheet
* @param fieldList 获取类中有映射关系的属性名
* @return
*/
private static <T> ArrayList<Map<String, String>> readExcel(int start, int size, Sheet sheet, ArrayList<String> fieldList) {
ArrayList<Map<String, String>> listMap = new ArrayList<>();
for (int i = start; i < start + size; i++) {
Map<String, String> map = new TreeMap<>();
for (int j = 0; j < fieldList.size(); j++) {
map.put(fieldList.get(j), sheet.getRow(i).getCell(j).getStringCellValue());
}
listMap.add(map);
}
return listMap;
}
/**
* 获取属性名
* @param clazz
* @param <T>
* @return
* @throws Exception
*/
private static <T> ArrayList<String> getFieldNameByExcelAnno(Class<T> clazz) throws Exception {
return getFieldWithExcel(clazz)
.map(field -> field.getName())
.collect(Collectors.toCollection(ArrayList::new));
}
/*------------------------------ListToExcel的调用方法------------------------------------*/
/**
* 写入内容
* @param list 数据内容
* @param sheet
* @param fieldMapper 属性转换
* @param <T>
*/
private static <T> void setContent(List<T> list, Sheet sheet, Map<String, Map<String, String>> fieldMapper, ExcelStyle contentStyle) {
Stream.iterate(0, item -> item + 1).limit(list.size())
.forEach(item -> {
//当前行
Row row = sheet.createRow(item + 1);
//每个对象的属性值
List<String> fieldValue = getFiledValueIfIsExcel(list.get(item), fieldMapper);
Stream.iterate(0, i -> i + 1)
.limit(fieldValue.size())
.forEach(i -> {
Cell cell = row.createCell(i);
cell.setCellType(CellType.STRING);
//cell.setCellStyle(getContentStyle(sheet.getWorkbook(), contentStyle));
cell.setCellValue(fieldValue.get(i));
});
});
}
/**
* 写入标题
* @param excelList 标题
* @param sheet
*/
private static void setHead(List<Excel> excelList, Sheet sheet, ExcelStyle headStyle) {
Row row = sheet.createRow(0);
Stream.iterate(0, item -> item + 1)
.limit(excelList.size())
.forEach(item -> {
Cell cell = row.createCell(item);
cell.setCellType(CellType.STRING);
cell.setCellValue(excelList.get(item).name());
cell.setCellStyle(getTitleStyle(sheet.getWorkbook(), headStyle));
sheet.setColumnWidth(item, excelList.get(item).width() * 2 << 6);
});
}
/**
* 设置头样式
* @param workbook
* @return
*/
private static CellStyle getTitleStyle(Workbook workbook, ExcelStyle headStyle) {
CellStyle cellStyle = workbook.createCellStyle();
headStyle = headStyle == null ? new ExcelStyle() : headStyle;
//对齐方式
cellStyle.setAlignment(headStyle.getAlign() == null ? HorizontalAlignment.CENTER : headStyle.getAlign());
//设置字体
Font font = workbook.createFont();
String fontName = headStyle.getFontName();
font.setFontName(null == fontName ? "黑体" : fontName);
//字体大小
font.setFontHeightInPoints(headStyle.getSize() <= 0 ? 14 : headStyle.getSize());
font.setBold(true);
font.setColor(headStyle.getFontColor() <= 0 ? HSSFColor.BLACK.index : headStyle.getFontColor());
cellStyle.setFont(font);
//背景
cellStyle.setFillForegroundColor(headStyle.getBackColor() <= 0
? HSSFColor.SEA_GREEN.index
: headStyle.getBackColor());
cellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
BorderStyle border = headStyle.getBorderStyle() == null ? BorderStyle.MEDIUM : headStyle.getBorderStyle();
//边框
cellStyle.setBorderLeft(border);
cellStyle.setBorderTop(border);
cellStyle.setBorderRight(border);
cellStyle.setBorderBottom(border);
//自动换行
//cellStyle.setWrapText(true);
return cellStyle;
}
/**
* 设置内容样式,占用过大,并没有开启这个功能
* @param workbook
* @return
*/
private static CellStyle getContentStyle(Workbook workbook, ExcelStyle contentStyle) {
contentStyle = contentStyle == null ? new ExcelStyle() : contentStyle;
CellStyle cellStyle = workbook.createCellStyle();
//对齐方式
cellStyle.setAlignment(contentStyle.getAlign() == null ? HorizontalAlignment.LEFT : contentStyle.getAlign());
//设置字体
Font font = workbook.createFont();
String fontName = contentStyle.getFontName();
font.setFontName(null == fontName ? "黑体" : fontName);
//字体大小
font.setFontHeightInPoints(contentStyle.getSize() <= 0 ? 12 : contentStyle.getSize());
font.setBold(contentStyle.isBold());
font.setColor(contentStyle.getFontColor() <= 0 ? HSSFColor.BLACK.index : contentStyle.getFontColor());
cellStyle.setFont(font);
//背景
cellStyle.setFillForegroundColor(contentStyle.getBackColor() <= 0
? HSSFColor.WHITE.index
: contentStyle.getBackColor());
cellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
BorderStyle border = contentStyle.getBorderStyle() == null ? BorderStyle.THIN : contentStyle.getBorderStyle();
//边框
cellStyle.setBorderLeft(border);
cellStyle.setBorderTop(border);
cellStyle.setBorderRight(border);
cellStyle.setBorderBottom(border);
//自动换行
//cellStyle.setWrapText(true);
return cellStyle;
}
/**
* 拿到一个类中属性上的Excel注解
* @param clazz
* @param <T>
* @return
* @throws Exception
*/
private static <T> List<Excel> getExcelAnnoList(Class<T> clazz) throws Exception {
return getFieldWithExcel(clazz)
.map(field -> field.getAnnotation(Excel.class))
.collect(Collectors.toCollection(ArrayList::new));
}
/**
* 获取带有Excel注解的属性
* @param clazz
* @param <T>
* @return
*/
private static <T> Stream<Field> getFieldWithExcel(Class<T> clazz) {
Field[] fields = clazz.getDeclaredFields();
return Arrays.stream(fields)
.filter(field -> {
Excel excel = field.getAnnotation(Excel.class);
return excel != null && !excel.skip();
});
}
/**
* 获取一个类包含Excel注解的属性的值
* @param t
* @param <T>
* @return
*/
private static <T> List<String> getFiledValueIfIsExcel(T t, Map<String, Map<String, String>> fieldMapper) {
return getFieldWithExcel(t.getClass()).map(field -> {
field.setAccessible(true);
Object o = null;
try {
o = field.get(t);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
//如果属性值为空,返回空字符串
if (Objects.isNull(o)) {
o = "";
}
//如果是Date类型
if (o instanceof Date) {
o = new SimpleDateFormat(field.getAnnotation(Excel.class).dateFormat()).format((Date) o);
}
//如果是LocalDateTime类型,支持Java8LocalDateTime
if (o instanceof LocalDateTime) {
o = ((LocalDateTime) o).format(DateTimeFormatter.ofPattern(field.getAnnotation(Excel.class).dateFormat()));
}
//bool和int属性转换,感觉最常用这两个,其它的需要再扩充
if (fieldMapper != null && (o instanceof Boolean || o instanceof Integer)) {
Map<String, String> map = fieldMapper.get(field.getName());
o = map == null ? o : map.get(o.toString()) == null ? o : map.get(o.toString());
}
//其它的返回toString
return o.toString();
}).collect(Collectors.toCollection(ArrayList::new));
}
}
八、使用方法
//把List转换为Excel
public static <T> void listToExcel(List<T> list, Path filePath, Map<String, Map<String, String>> fieldMapper, ExcelStyle headStyle, ExcelStyle contentStyle) {}
参数说明:
1. list: 要转换的类(类中需要转换的属性需要加上Excel注解)
2. filePath: Java7中的文件操作类。Excel导出的地址
3. fieldMapper: 属性转换(可以为null)
4. headStyle: 头部样式(可以为null),为空时使用默认样式。
5. contentStyle:内容体的样式(可以为null),这个默认不生效(由于这个功能占用执行时间过多(大约十倍),所以在205行注释掉了,有需要可以打开)
//把Excel转换为List
excelToList(Path filePath, Class<T> clazz, Map<String, Map<String, String>> fieldMapper){}
参数说明:
1. filePath: Java7中的文件操作类。Excel导出的地址
2. clazz: 要转换的类
3. fieldMapper: 属性转换(可以为null)
4. 如果有日期等,需要在类属性的Excel注解上给dateFormat属性赋值。
九、测试用例
//基础类
public class ExcelDTO {
@Excel(name = "姓名", width = 30)
private String name;
@Excel(name = "年龄", width = 30)
private Integer age;
@Excel(skip = true)
private String password;
@Excel(name = "生日", dateFormat = "yyyy年MM/dd HH:mm", width = 50)
private Date birth;
@Excel(name = "帐户")
private Double money;
@Excel(name = "冻结")
private Boolean lock;
//getter,setter
}
测试
/**
* @author kingboy--KingBoyWorld@163.com
* @date 2017/7/26 下午2:04
* @desc 测试我写的Excel工具类.
*/
public class MyExcelTest {
/**
* 导出为Excel,将属性进行转换。
* @throws Exception
*/
@Test
public void ListToExcel_test() throws Exception {
long start = System.nanoTime();
//属性转换
Map<String, Map<String, String>> map = new HashMap<>();
//age的转换
Map<String, String> value = new HashMap<>();
value.put("12", "12岁");
value.put("15", "15岁");
value.put("18", "18岁");
map.put("age", value);
//lock的转换
Map<String, String> lock = new HashMap<>();
lock.put("true", "锁");
lock.put("false", "正常");
map.put("lock", lock);
ExcelUtils.listToExcel(getExcelDTOList(), Paths.get("/Users/kingboy/Desktop/My.xlsx"), map, null, null);
System.out.println("-----my-------" + (System.nanoTime() - start) / 1000_000 + "毫秒");
}
/**
* 导出为Excel,没有将属性进行转换。
* @throws Exception
*/
@Test
public void ListToExcelNoMapper_test() throws Exception {
//没有转换属性测试
ExcelUtils.listToExcel(getExcelDTOList(), Paths.get("/Users/kingboy/Desktop/My1.xlsx"), null, new ExcelStyle().setBackColor(HSSFColor.WHITE.index),new ExcelStyle().setAlign(HorizontalAlignment.CENTER));
}
/**
* 导入为List,将属性进行转换。
* 注意属性转换是反过来的。
* @throws Exception
*/
@Test
public void ExcelToList_test() throws Exception {
long start = System.nanoTime();
Map<String, Map<String, String>> map = new HashMap<>();
Map<String, String> value = new HashMap<>();
value.put("小", "12");
value.put("中", "15");
value.put("大", "18");
Map<String, String> lock = new HashMap<>();
lock.put("锁", "true");
lock.put("正常", "false");
map.put("age", value);
map.put("lock", lock);
List<ExcelDTO> excelDTOS = ExcelUtils.excelToList(Paths.get("/Users/kingboy/Desktop/My.xlsx"), ExcelDTO.class, map);
System.out.println("----my--------" + (System.nanoTime() - start) / 1000_000 + "毫秒");
}
public List<ExcelDTO> getExcelDTOList() {
List<ExcelDTO> list = new ArrayList<>();
Stream.iterate(1, item -> item + 1).limit(10000)
.forEach(item -> list.add(new ExcelDTO("king1", 15, "1123", new Date(), 111d, true)));
return list;
}
}
导出的效果图:
有属性转换的效果:
无属性转换的效果: