問題背景
最近遇到的需求:用戶填寫平臺提供的模板文件(sample.xlsx)倍靡,導(dǎo)入到平臺中辈双,代替填寫表單/表格的動作挽绩。用戶也可以將填好的表單/表格導(dǎo)出成Excel文件伞广,便于以文檔的形式存檔或傳輸汁果。
問題分析
從上述需求得出涡拘,這就是一個(gè)Excel文檔與Java Bean
對象互相轉(zhuǎn)換的問題。
Excel文件的讀寫据德、操作鳄乏,可以使用Apache Poi
或其他開源庫,在此不多說棘利。主要問題是橱野,當(dāng)模板文件內(nèi)容較為復(fù)雜,或者需要處理多個(gè)模板時(shí)善玫,怎樣能快速解析出文檔的內(nèi)容仲吏,與Java Bean
的字段對應(yīng)上呢?
應(yīng)用Java反射的原理蝌焚,使用自定義注解 + 泛型裹唆,很容易實(shí)現(xiàn)靈活適配多個(gè)模板,并能快速支持模板的修改只洒,便于擴(kuò)展和維護(hù)许帐。
模板文件分析
分析模板文件,我發(fā)現(xiàn)可以將模板分為兩種毕谴。
-
表單式
內(nèi)容行標(biāo)成畦、列標(biāo)均確定,例如該表格B1固定為姓名的值涝开,將內(nèi)容解析成單個(gè)JavaBean對象循帐。
-
表格式
內(nèi)容列固定,例如A列均為學(xué)號舀武,應(yīng)將除去表頭外的每一行解析成一個(gè)JavaBean 對象拄养,返回Java Bean對象的列表。
分析完畢银舱,發(fā)現(xiàn)Java Bean 對象的某個(gè)字段的值與Excel文檔單元格內(nèi)容的對應(yīng)關(guān)系就是行標(biāo) + 列標(biāo)瘪匿,那么我們只需要記錄這個(gè)坐標(biāo),就能實(shí)現(xiàn)轉(zhuǎn)換寻馏。
使用在字段上加注解的方式棋弥,簡明易懂,并且很容易維護(hù)诚欠。下面給出實(shí)現(xiàn)代碼顽染。
實(shí)現(xiàn)
1漾岳、定義注解類
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ExcelAnnotation {
/**
* 中文名稱 label
*/
String cnName() default "";
/**
* 英文名稱 對應(yīng)到JavaClass上的類名
*/
String enName() default "";
/**
*
文件中的行標(biāo) - 從0開始計(jì)數(shù)
*/
int rowIndex() default -1;
int sheetIndex() default -1;
/**
*
* 文件中的列標(biāo) - 從0開始計(jì)數(shù)
*/
int columnIndex() default -1;
}
注解用來說明字段對應(yīng)的單元格工作簿序號、行標(biāo)粉寞、列標(biāo)等信息尼荆,以及
2、定義Java Bean
- 表單式對應(yīng)的對象
@Data
class Person{
@ExcelAnnotation(rowIndex = 0,columnIndex = 1,cnName = "姓名")
private String name;
@ExcelAnnotation(rowIndex = 2,columnIndex = 1,cnName = "電話號碼")
private String phoneNum;
...
}
- 表格式對應(yīng)的對象
只需要定義列的中文名(cnName),不需要定義行標(biāo)
@Data
class Student{
@ExcelAnnotation(cnName = "學(xué)號")
private String number;
@ExcelAnnotation(cnName = "姓名")
private String name;
@ExcelAnnotation(cnName = "電話號碼")
private String phoneNum;
}
3仁锯、工具類實(shí)現(xiàn)寫入和寫出
定義Excel操作的工具類
ExcelUtils.java
@Log4j2
public class ExcelUtils {
public static <T> List<T> analysisExcelSheetAsTable(Sheet sheet,Class<T> clazz,int headerIndex) throws IntrospectionException {
ArrayList<Row> rowContent = new ArrayList<>();
TreeMap<Integer, Method> writeMethodTreeMap = new TreeMap<>();
// 記錄表頭內(nèi)容與
HashMap<String,Integer> headerCnNameMap = new HashMap<>();
// 默認(rèn)的表頭數(shù)據(jù)
// 獲取表頭數(shù)據(jù)
Row tableHeader = sheet.getRow(headerIndex);
//
int index = 0;
for(Cell headerCell: tableHeader){
String headerContent = ExcelUtils.getCellFormatValue(headerCell).toString().trim();
headerCnNameMap.put(headerContent,index);
index++;
}
// 忽略第一行表頭數(shù)據(jù)
for (int i = (headerIndex+1); 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);
// 從頭部獲取cnName
if(headerCnNameMap.containsKey(excelAnnotation.cnName())){
writeMethodTreeMap.put(headerCnNameMap.get(excelAnnotation.cnName()), pd.getWriteMethod());
}
} catch (IntrospectionException e) {
throw e;
}
}
}
}
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.isEmpty(rowValue)) {
for(int cellIndex: writeMethodTreeMap.keySet()){
if(row.getCell(cellIndex) != null){
Cell cell = row.getCell(cellIndex);
String value = ExcelUtils.getCellFormatValue(cell).toString();
writeMethodTreeMap.get(cellIndex).invoke(model, value);
}
}
resultList.add(model);
}
} catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
}
}
return resultList;
}
/**
* 解析Excel表格內(nèi)容 - 按照表格解析
* @param sheet
* @param ignoreRowNum 忽略的行數(shù)(表頭)
* @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.isEmpty(rowValue)) {
// 遍歷格子
int i = 0;
for (Cell cell : row) {
if (!writeMethodTreeMap.containsKey(i)) {
i++;
continue;
}
String value = ExcelUtils.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))
{
//轉(zhuǎn)換為日期格式Y(jié)YYY-mm-dd
cellValue = cell.getDateCellValue();
}
else
{
//數(shù)字
cellValue = cell.getNumericCellValue();
}
break;
}
case STRING:
{
cellValue = cell.getRichStringCellValue().getString();
break;
}
default:
cellValue = "";
}
return cellValue;
}
}
4耀找、調(diào)用
在業(yè)務(wù)類(Service
)中調(diào)用
- 調(diào)用僅為示范
// 導(dǎo)入
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);
// 僅示范調(diào)用方式,可自行返回
} catch (Exception ex) {
log.error("Excel解析失斠笛隆野芒!",ex);
throw new BusinessException("Excel解析失敗双炕!");
}
}
//導(dǎo)出
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.根據(jù)查詢到的內(nèi)容妇斤,填充Excel表格內(nèi)容
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的一些格式、編碼/解碼方式站超,我們可以放在通用配置里荸恕,也可以在注解類中增加項(xiàng)目來應(yīng)對特殊需求。
總結(jié)
本篇涉及知識
1死相、泛型
2融求、自定義注解 + Java
反射
3、Apache Poi
的使用
- 為什么使用泛型算撮?
1生宛、將數(shù)據(jù)類型和算法進(jìn)行剝離是一種很常用的設(shè)計(jì)思路,可以幫助我們更好地開發(fā)出通用方法肮柜。
2陷舅、使用泛型(而不是包容萬物的Object類型)使代碼更為可讀,并能規(guī)避編譯錯(cuò)誤审洞,還可以對傳入類型的上界莱睁、下界進(jìn)行規(guī)定。(super/extends)