问题背景
最近遇到的需求:用户填写平台提供的模板文件(sample.xlsx),导入到平台中,代替填写表单/表格的动作。用户也可以将填好的表单/表格导出成Excel文件,便于以文档的形式存档或传输。
问题分析
从上述需求得出,这就是一个Excel文档与Java Bean
对象互相转换的问题。
Excel文件的读写、操作,可以使用Apache Poi
或其他开源库,在此不多说。主要问题是,当模板文件内容较为复杂,或者需要处理多个模板时,怎样能快速解析出文档的内容,与Java Bean
的字段对应上呢?
应用Java反射的原理,使用自定义注解 + 泛型,很容易实现灵活适配多个模板,并能快速支持模板的修改,便于扩展和维护。
模板文件分析
分析模板文件,我发现可以将模板分为两种。
-
表单式
表单式
内容行标、列标均确定,例如该表格B1固定为姓名的值,将内容解析成单个JavaBean对象。
-
表格式
表格式
内容列标固定,例如A列均为学号,应将除去表头外的每一行解析成一个JavaBean 对象,返回Java Bean对象的列表。
分析完毕,发现Java Bean 对象的某个字段的值与Excel文档单元格内容的对应关系就是行标 + 列标,那么我们只需要记录这个坐标,就能实现转换。
使用在字段上加注解的方式,简明易懂,并且很容易维护。下面给出实现代码。
实现
1、定义注解类
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ExcelAnnotation {
/**
* 中文名称 label
*/
String cnName() default "";
/**
* 英文名称 对应到JavaClass上的类名
*/
String enName() default "";
/**
*
文件中的行标 - 从0开始计数
*/
int rowIndex() default -1;
int sheetIndex() default -1;
/**
*
* 文件中的列标 - 从0开始计数
*/
int columnIndex() default -1;
}
注解用来说明字段对应的单元格工作簿序号、行标、列标等信息,以及
2、定义Java Bean
- 表单式对应的对象
@Data
class Person{
@ExcelAnnotation(rowIndex = 0,columnIndex = 1,cnName = "姓名")
private String name;
@ExcelAnnotation(rowIndex = 2,columnIndex = 1,cnName = "电话号码")
private String phoneNum;
...
}
- 表格式对应的对象
只需要定义列标,不需要定义行标
@Data
class Student{
@ExcelAnnotation(columnIndex = 0,cnName = "学号")
private String number;
@ExcelAnnotation(columnIndex = 1,cnName = "姓名")
private String name;
@ExcelAnnotation(rowIndex = 2,columnIndex = 1,cnName = "电话号码")
private String phoneNum;
}
3、工具类实现写入和写出
定义Excel操作的工具类
ExcelUtils.java
@Log4j2
public class ExcelUtil {
/**
* 解析 -按照表单解析 - 所有内容占据并且仅占据一个固定单元格
* @param sheet 表单
* @param clazz 泛型的类型
* @param <E> 泛型
* @return 泛型实例
* @throws IllegalAccessException
* @throws InstantiationException
*/
public static <E> E analysisExcelSheetAsForm(Sheet sheet, Class<E> clazz) throws IllegalAccessException, InstantiationException {
// 获取最大行数
int rowNum = sheet.getPhysicalNumberOfRows();
Map<String, Object> map = new HashMap<>();
// 获取开始行(0 + bgnIgnore)
Row titleRow = sheet.getRow(0);
if (null == titleRow) {
return null;
}
// 获取明细列数
int colNum = titleRow.getPhysicalNumberOfCells();
//
int rowBgn = 0;
// 遍历明细
for (int rowIndex = rowBgn; rowIndex < rowNum; rowIndex++) {
// 行信息
Row row = sheet.getRow(rowIndex);
if (row != null) {
// 遍历行并获取到返回值
for (int cellIndex = 0; cellIndex < colNum; cellIndex++) {
Cell cell = row.getCell(cellIndex);
// 位置为 行号 + 列号
String position = rowIndex + "_" +cellIndex;
// 数据
Object cellData = ExcelUtil.getCellFormatValue(cell);
// 将数据存储到Map中
map.put(position, cellData);
}
} else {
break;
}
}
E model = clazz.newInstance();
// 获取类字段
Field[] fields = model.getClass().getDeclaredFields();
for (Field field : fields) {
// 获取字段上的注解
Annotation[] annotations = field.getAnnotations();
if (annotations.length == 0) {
continue;
}
for (Annotation an : annotations) {
field.setAccessible(true);
// 若扫描到ExcelAnnotation注解
if (an.annotationType().getName().equals(ExcelAnnotation.class.getName())) {
// 获取指定类型注解
ExcelAnnotation column = field.getAnnotation(ExcelAnnotation.class);
String mappedKey = column.rowIndex() + "_" + column.columnIndex();
// 获取model属性的类型
Class<?> modelType = field.getType();
// 获取map中的数据
Object value = map.get(mappedKey);
// 匹配字段类型
if (null != value) {
// 获取map中存主的字段的类型
Class<?> cellType = value.getClass();
// 处理int类型不匹配问题
if (cellType == String.class && (modelType == int.class || modelType == Integer.class)) {
value = Integer.valueOf(value.toString());
}
// 处理double/bigDecimal类型不匹配问题
if (cellType == String.class && modelType == BigDecimal.class) {
// 不使用bigDecimal(double),否则bigDecimal(0.1)有惊喜
value = new BigDecimal(value.toString());
}
if (cellType == String.class && modelType == double.class) {
value = Double.valueOf(value.toString());
}
// 处理String类型不匹配问题
if (cellType != String.class && modelType == String.class) {
value = value.toString();
}
}
field.set(model, value);
}
}
field.setAccessible(false);
}
return model;
}
/**
* 解析Excel表格内容 - 按照表格解析
* @param sheet
* @param ignoreRowNum 忽略的行数(表头)
* @param <T>
* @return
*/
public static <T> List<T> analysisExcelSheetAsTable(Sheet sheet,int ignoreRowNum,Class<T> clazz) {
ArrayList<Row> rowContent = new ArrayList<>();
TreeMap<Integer, Method> writeMethodTreeMap = new TreeMap<>();
// 从忽略的表头开始读
for (int i = ignoreRowNum; i < sheet.getPhysicalNumberOfRows(); i++) {
rowContent.add(sheet.getRow(i));
}
for (Field field : clazz.getDeclaredFields()) {
// 获取字段上的注解
Annotation[] annotations = field.getAnnotations();
if (annotations.length == 0) {
continue;
}
for (Annotation an : annotations) {
// 若扫描到ExcelAnnotation注解
if (an.annotationType().getName().equals(ExcelAnnotation.class.getName())) {
// 获取指定类型注解
ExcelAnnotation excelAnnotation = field.getAnnotation(ExcelAnnotation.class);
try {
// 获取该字段的method方法
PropertyDescriptor pd = new PropertyDescriptor(field.getName(), clazz);
writeMethodTreeMap.put(excelAnnotation.columnIndex(), pd.getWriteMethod());
} catch (IntrospectionException e) {
e.printStackTrace();
}
}
}
}
DataFormatter dataFormatter = new DataFormatter();
List<T> resultList = new ArrayList<>();
for (Row row : rowContent) {
String rowValue = dataFormatter.formatCellValue(row.getCell(0));
try {
T model = clazz.newInstance();
if (StringUtils.isNotBlank(rowValue)) {
// 遍历格子
int i = 0;
for (Cell cell : row) {
if (!writeMethodTreeMap.containsKey(i)) {
i++;
continue;
}
String value = getCellFormatValue(cell).toString();
writeMethodTreeMap.get(i).invoke(model, value);
i++;
}
resultList.add(model);
}
} catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
}
}
return resultList;
}
/**
* 读取Cell
*
* @param cell
* @return
*/
public static Object getCellFormatValue(Cell cell)
{
Object cellValue;
//判断cell类型
switch (cell.getCellTypeEnum())
{
case NUMERIC:
{
cellValue = cell.getNumericCellValue();
break;
}
case FORMULA:
{
//判断cell是否为日期格式
if (DateUtil.isCellDateFormatted(cell))
{
//转换为日期格式YYYY-mm-dd
cellValue = cell.getDateCellValue();
}
else
{
//数字
cellValue = cell.getNumericCellValue();
}
break;
}
case STRING:
{
cellValue = cell.getRichStringCellValue().getString();
break;
}
default:
cellValue = "";
}
return cellValue;
}
/**
*向 Excel 文件中写入
*/
public static <T> void writeToWorkbookAsForm(T model, Sheet sheet){
// 若传入参数有的为空,直接返回
if(model == null || sheet == null){
return;
}
// 获取类字段
Field[] fields = model.getClass().getDeclaredFields();
for (Field field : fields) {
// 获取字段上的注解
Annotation[] annotations = field.getAnnotations();
if (annotations.length == 0) {
continue;
}
for (Annotation an : annotations) {
field.setAccessible(true);
// 若扫描到ExcelAnnotation注解
if (an.annotationType().getName().equals(ExcelAnnotation.class.getName())) {
// 获取指定类型注解
ExcelAnnotation column = field.getAnnotation(ExcelAnnotation.class);
try {
String value = (String) field.get(model);
writeToCell(sheet,column.rowIndex(),column.columnIndex(),value);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
field.setAccessible(false);
}
}
/**
* 读入类型 * * @param tIterable
* @param sheet 表单
* @param ignoredRowNum 忽略的表头行数
* @param <T> 泛型类型
*/
public static <T> void writeToWorkbookAsTable(Iterable<T> tIterable, Sheet sheet,int ignoredRowNum,Class<T> clazz){
// 若传入参数有的为空,直接返回
if(tIterable == null || sheet == null){
return;
}
// 获取类字段
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
// 获取字段上的注解
Annotation[] annotations = field.getAnnotations();
if (annotations.length == 0) {
continue;
}
for (Annotation an : annotations) {
field.setAccessible(true);
// 若扫描到ExcelAnnotation注解
if (an.annotationType().getName().equals(ExcelAnnotation.class.getName())) {
// 获取指定类型注解
ExcelAnnotation column = field.getAnnotation(ExcelAnnotation.class);
// 行号
AtomicInteger rowIndex = new AtomicInteger(ignoredRowNum);
// 遍历赋值按列写入
tIterable.forEach(item -> {
try {
String value= (String) field.get(item);
writeToCell(sheet, rowIndex.get(),column.columnIndex(),value);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
rowIndex.getAndIncrement();
});
}
}
field.setAccessible(false);
}
}
/**
* 向指定格子写入
* @param rowIndex
* @param columnIndex
*/
static void writeToCell(Sheet sheet, int rowIndex, int columnIndex, String cellValue){
Row row = sheet.getRow(rowIndex) == null?sheet.createRow(rowIndex):sheet.getRow(rowIndex);
Cell cell = row.getCell(columnIndex) == null?row.getCell(columnIndex):row.createCell(columnIndex);
cell.setCellValue(cellValue);
}
}
4、调用
在业务类(Service
)中调用
- 调用仅为示范
// 导入
public void importExcelFile(InputStream inputStream){
try (Workbook workbook = WorkbookFactory.create(inputStream)) {
Person person= ExcelUtil.analysisExcelSheetAsForm(workbook.getSheetAt(0),Person.class);
List<Student> students= ExcelUtil.analysisExcelSheetAsTable(workbook.getSheetAt(1),1,Student.class);
// 仅示范调用方式,可自行返回
} catch (Exception ex) {
log.error("Excel解析失败!",ex);
throw new BusinessException("Excel解析失败!");
}
}
//导出
public void exportExcelFile(List<Student> students,Person person,HttpServletResponse httpServletResponse){
//1、读取excel模板文件,作为本次下载的模板
try(InputStream inputStream = new FileInputStream(templatePath);
Workbook workbook = WorkbookFactory.create(inputStream)){
httpServletResponse.setContentType("application/octet-stream");
httpServletResponse.setHeader("Content-Disposition", "attachment;filename=test.xlsx");
// 2.根据查询到的内容,填充Excel表格内容
ExcelUtil.writeToWorkbookAsForm(person,workbook.getSheetAt(0));
ExcelUtil.writeToWorkbookAsTable(students,workbook.getSheetAt(1,1,Student.class);
workbook.write(httpServletResponse.getOutputStream());
} catch (IOException | InvalidFormatException e) {
e.printStackTrace();
}
}
这样就完成啦。
对于写入Excel和读取Excel的一些格式、编码/解码方式,我们可以放在通用配置里,也可以在注解类中增加项目来应对特殊需求。
总结
本篇涉及知识
1、泛型
2、自定义注解 + Java
反射
3、Apache Poi
的使用
- 为什么使用泛型?
1、将数据类型和算法进行剥离是一种很常用的设计思路,可以帮助我们更好地开发出通用方法。
2、使用泛型(而不是包容万物的Object类型)使代码更为可读,并能规避编译错误,还可以对传入类型的上界、下界进行规定。(super/extends)
网友评论