动态级联下拉模板导出
前言
在项目中需要动态的生成带级联下拉的excel模板,数据来源为mysql数据库,因为不需要产生过程中的临时文件,所有楼主是使用了直接返回前端文件流的形式。
本文提供了
1、直接返回流的形式
2、生成本地文件之后返回文件路径
注:因为是基于excel的名称管理器和poi实现,在poi设置名称管理器的名字存在字符验证。如下的验证,所以对于作为下拉的数据有相应的要求。必须是汉字、数字、字母、下划线和点且不能是数字和点开头。
private static void validateName(String name) {
if (name.length() == 0) {
throw new IllegalArgumentException("Name cannot be blank");
}
if (name.length() > 255) {
throw new IllegalArgumentException("Invalid name: '"+name+"': cannot exceed 255 characters in length");
}
if (name.equalsIgnoreCase("R") || name.equalsIgnoreCase("C")) {
throw new IllegalArgumentException("Invalid name: '"+name+"': cannot be special shorthand R or C");
}
// is first character valid?
char c = name.charAt(0);
String allowedSymbols = "_\\";
boolean characterIsValid = (Character.isLetter(c) || allowedSymbols.indexOf(c) != -1);
if (!characterIsValid) {
throw new IllegalArgumentException("Invalid name: '"+name+"': first character must be underscore or a letter");
}
// are all other characters valid?
allowedSymbols = "_.\\"; //backslashes needed for unicode escape
for (final char ch : name.toCharArray()) {
characterIsValid = (Character.isLetterOrDigit(ch) || allowedSymbols.indexOf(ch) != -1);
if (!characterIsValid) {
throw new IllegalArgumentException("Invalid name: '"+name+"': name must be letter, digit, period, or underscore");
}
}
// Is the name a valid $A$1 cell reference
// Because $, :, and ! are disallowed characters, A1-style references become just a letter-number combination
if (name.matches("[A-Za-z]+\\d+")) {
String col = name.replaceAll("\\d", "");
String row = name.replaceAll("[A-Za-z]", "");
try {
if (CellReference.cellReferenceIsWithinRange(col, row, SpreadsheetVersion.EXCEL2007)) {
throw new IllegalArgumentException("Invalid name: '"+name+"': cannot be $A$1-style cell reference");
}
} catch (final NumberFormatException e) {
// row was not parseable as an Integer, such as a BigInt
// therefore name passes the not-a-cell-reference criteria
}
}
// Is the name a valid R1C1 cell reference?
if (name.matches("[Rr]\\d+[Cc]\\d+")) {
throw new IllegalArgumentException("Invalid name: '"+name+"': cannot be R1C1-style cell reference");
}
}
一、构建项目结构
二、最终效果(下载到本地,流的形式可自行测试)
三、具体实现
1)pom.xml 依赖
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.gongl</groupId>
<artifactId>poi-select</artifactId>
<version>1.0</version>
<name>poi-select</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- SpringBoot的依赖配置-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.5.8</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- excel工具 -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>4.1.2</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>4.1.2</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml-schemas</artifactId>
<version>4.1.2</version>
</dependency>
<!-- servlet包 -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
<!-- log4j日志组件 -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.17.1</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-to-slf4j</artifactId>
<version>2.17.1</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.9</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
2)注解类 Excel 和 ExcelFile
package com.gongl.annotation;
import org.apache.poi.ss.usermodel.DataValidationConstraint;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author gongl
* @date 2022-08-16
*/
@Target({
ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Excel {
/**
* 列名
*/
String name() default "";
int columnWidth() default (int) ((22 + 0.72) * 256);
/**
* 排序
*/
int order();
/**
* 是否支持动态列,true:表示该列无数据时,不添加到表格
*/
boolean dynamic() default false;
/**
* 数据源方法全名
*/
String datasourceMethod() default "";
/**
* 前列字段
* 当为空字符串时,认定无前列依赖
*/
String beforeFieldName() default "";
/**
* 开始行
*/
int firstRow() default 2;
/**
* 结束行
*/
int lastRow() default 200;
/**
* 校验类型
*/
int validationType() default DataValidationConstraint.ValidationType.LIST;
}
package com.gongl.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author gongl
* @date 2022-08-16
*/
@Target({
ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ExcelFile {
/**
* 文件全名
*/
String fileName() default "excel.xlsx";
/**
* 页名称
*/
String sheetName() default "sheet1";
/**
* 为true时,创建数据页,当不存在级联下拉时可设置为不创建
*/
boolean enableDataValidation() default false;
/**
* 数据页名称
*/
String dataSheetName() default "dataSheet";
/**
* 隐藏数据页
*/
boolean datasheetHidden() default true;
}
3)构建实体类 TestTemplate ,添加注解
package com.gongl.base;
import com.gongl.annotation.Excel;
import com.gongl.annotation.ExcelFile;
/**
* @author gongl
* @date 2022-08-16
*/
@ExcelFile(enableDataValidation = true, fileName = "测试级联下拉excel.xlsx")
public class TestTemplate {
@Excel(name = "姓名", order = 0)
private String username;
@Excel(name = "所属省", order = 1, datasourceMethod = "com.gongl.service.ExcelDataService.getProvinceList")
private String province;
@Excel(name = "所属市", order = 2, datasourceMethod = "com.gongl.service.ExcelDataService.getCityList",
beforeFieldName = "province")
private String city;
@Excel(name = "性别", order = 3, datasourceMethod = "com.gongl.service.ExcelDataService.getSexList")
private String sex;
@Excel(name = "年龄", order = 4)
private Integer age;
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getProvince() {
return province;
}
public void setProvince(String province) {
this.province = province;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
}
4)构建数据获取类 ExcelDataService
package com.gongl.service;
import java.util.*;
/**
* @author gongl
* @date 2022-08-16
*/
public class ExcelDataService {
//实际开发中,可通过查询数据库获取数据
public List<String> getProvinceList(Map<String, Object> map) {
return Arrays.asList("四川省","广东省");
}
public Map<String, List<String>> getCityList(Map<String, Object> map) {
Map<String, List<String>> map1 = new HashMap<>();
map1.put("四川省",Arrays.asList("南充市","眉山市","成都市"));
map1.put("广东省",Arrays.asList("惠州市","中山市","广州市"));
return map1;
}
public List<String> getSexList(Map<String, Object> map) {
return Arrays.asList("男","女","未知");
}
}
5)构建工具类 ExcelUtils
package com.gongl.utils;
import com.gongl.annotation.Excel;
import com.gongl.annotation.ExcelFile;
import com.gongl.service.ExcelDataService;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.ss.util.CellRangeAddressList;
import org.apache.poi.util.IOUtils;
import org.apache.poi.xssf.usermodel.XSSFDataValidationConstraint;
import org.apache.poi.xssf.usermodel.XSSFDataValidationHelper;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.http.HttpServletResponse;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.net.URLEncoder;
import java.util.*;
import java.util.stream.Collectors;
/**
* @author gongl
* @date 2022-08-16
*/
public class ExcelUtils {
private static final Logger log = LoggerFactory.getLogger(ExcelUtils.class);
/**
* 样式列表
*/
private static Map<String, CellStyle> styles;
//直接下载文件到本地
public static void createExcel(Object object, String title, Map<String, Boolean> dynamicMap, Map<String, Object> params) throws IOException {
createExcel(null, object, title, dynamicMap, params);
}
/**
* 直接返回文件流到前端
* @param response
* @param object 实体对象
* @param title 文件表标题
* @param dynamicMap 需要动态展示的数据
* @param params 接口的参数
* @throws IOException
*/
public static void createExcel(HttpServletResponse response, Object object, String title, Map<String, Boolean> dynamicMap, Map<String, Object> params) throws IOException {
// 取到模板上的所有对象属性
Class<?> clazz = object.getClass();
List<Field> tempFields = new ArrayList<>();
tempFields.addAll(Arrays.asList(clazz.getSuperclass().getDeclaredFields()));
tempFields.addAll(Arrays.asList(clazz.getDeclaredFields()));
//得到所有的字段名和模板名
List<NameMapping> list = new ArrayList<>(16);
//得到当前需要导出的列注解
for (Field field : tempFields) {
if (field.isAnnotationPresent(Excel.class)) {
Excel attr = field.getAnnotation(Excel.class);
if (attr != null) {
field.setAccessible(true);
if (null != dynamicMap) {
Boolean bl = dynamicMap.get(field.getName());
if (attr.dynamic() && bl != null && !bl) {
//支持动态,且为动态
continue;
}
}
list.add(new NameMapping(field.getName(), attr));
}
}
}
//将数据按order升序
list = list.stream().sorted(Comparator.comparing(x -> x.getExcel().order())).collect(Collectors.toList());
//所有的参数顺序
List<String> fieldList = list.stream().map(NameMapping::getFieldName).collect(Collectors.toList());
//方法的参数
if (params == null) {
params = new HashMap<>(1);
}
boolean annotationPresent = clazz.isAnnotationPresent(ExcelFile.class);
if (!annotationPresent) {
throw new RuntimeException("不存在ExcelFile注解,不能生成Excel!");
}
ExcelFile excelFile = clazz.getAnnotation(ExcelFile.class);
// 创建excel
Workbook workbook = WorkbookFactory.create(true);
// 创建excel页
Sheet sheet = workbook.createSheet(excelFile.sheetName());
// 创建数据页
Sheet dataSheet = null;
if (excelFile.enableDataValidation()) {
// 创建数据页
dataSheet = workbook.createSheet(excelFile.dataSheetName());
// 设置隐藏属性
workbook.setSheetHidden(workbook.getSheetIndex(dataSheet), excelFile.datasheetHidden());
}
Map<String, Field> fieldMap = ReflectionUtil.getFieldMap(object);
int rowIndex = 0, colIndex = 0;
styles = createStyles(workbook);
//标题
Row titleRow = sheet.createRow(rowIndex);
titleRow.setHeightInPoints(50);
Cell titleCell = titleRow.createCell(0);
titleCell.setCellValue(title);
titleCell.setCellStyle(styles.get("title"));
sheet.addMergedRegion(new CellRangeAddress(titleRow.getRowNum(), titleRow.getRowNum(), titleRow.getRowNum(), list.size() - 1));
++rowIndex;
//创建表头和下拉
for (NameMapping nameMapping : list) {
String fieldName = nameMapping.getFieldName();
Excel excel = nameMapping.getExcel();
sheet.setColumnWidth(colIndex, excel.columnWidth());
//创建级联的下拉
if (excelFile.enableDataValidation()) {
createColumnValidation(fieldList, excel, fieldMap.get(fieldName), workbook, sheet, dataSheet, colIndex, params);
}
//创建表头
createCell(0, sheet, rowIndex, colIndex++, excel.name());
}
if (null != response) {
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
response.setHeader("Access-Control-Expose-Headers", "Content-Disposition");
response.setHeader("Content-Disposition", "attachment;filename=" + new String(URLEncoder.encode(excelFile.fileName(), "utf-8")));
response.setHeader("Access-Control-Allow-Origin", "*");
try {
workbook.write(response.getOutputStream());
} catch (Exception e) {
log.error("导出Excel异常{}", e.getMessage());
} finally {
IOUtils.closeQuietly(workbook);
}
} else {
//若需要生成指定文件路径,在这里自定义即可
FileOutputStream out = new FileOutputStream(excelFile.fileName());
workbook.write(out);
out.close();
}
}
private static void createColumnValidation(List<String> fieldList, Excel value, Field field, Workbook workbook, Sheet sheet, Sheet dataSheet, int colIndex,
Map<String, Object> params) {
if (field == null || dataSheet == null) {
return;
}
field.setAccessible(true);
String datasourceMethod = value.datasourceMethod();
if ("".equals(datasourceMethod)) {
return;
}
Object invoke;
try {
invoke = ReflectionUtil.getMethod(datasourceMethod, new ExcelDataService(), params);
} catch (IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
return;
}
String formulaIndirectFormat = "=INDIRECT(%s!$%s$%s)";
// 判断是否有前置字段
if (StringUtils.isBlank(value.beforeFieldName())) {
if (!(invoke instanceof Collection)) {
return;
}
Collection collection = (Collection) invoke;
//创建下拉的名称管理器
int nameManage = createNameManage(workbook, dataSheet, field.getName(), collection);
String formulaIndirect = String.format(formulaIndirectFormat, dataSheet.getSheetName(), getCellColumnFlag(1), nameManage + 1);
createDataValidate(sheet, formulaIndirect, value.validationType(), value.firstRow(), value.lastRow(), colIndex, colIndex);
} else {
if (!(invoke instanceof Map)) {
return;
}
Map<String, Collection> map = (Map<String, Collection>) invoke;
map.forEach((k, v) -> createNameManage(workbook, dataSheet, k, v));
//得到前置字段在第几列
int beforeColIndex = 0;
for (int i = 0; i < fieldList.size(); i++) {
if (fieldList.get(i).equals(value.beforeFieldName())) {
beforeColIndex = i;
break;
}
}
for (int rowIndex = value.firstRow(); rowIndex <= value.lastRow(); rowIndex++) {
String formulaIndirect = String.format(formulaIndirectFormat, sheet.getSheetName(), getCellColumnFlag(beforeColIndex + 1), rowIndex + 1);
createDataValidate(sheet, formulaIndirect, value.validationType(), rowIndex, rowIndex, colIndex, colIndex);
}
}
}
private static void createDataValidate(Sheet sheet, String formula, int validationType, int firstRow, int lastRow, int firstCol, int lastCol) {
CellRangeAddressList cellRangeAddressList = new CellRangeAddressList(firstRow, lastRow, firstCol, lastCol);
XSSFDataValidationHelper xssfDataValidationHelper = new XSSFDataValidationHelper((XSSFSheet) sheet);
XSSFDataValidationConstraint xssfDataValidationConstraint = new XSSFDataValidationConstraint(validationType, formula);
DataValidation validation = xssfDataValidationHelper.createValidation(xssfDataValidationConstraint, cellRangeAddressList);
validation.createErrorBox("输入有误!", "请选择下拉菜单里面的选项!");
validation.setEmptyCellAllowed(false);
validation.setShowErrorBox(true);
sheet.addValidationData(validation);
}
private static int createNameManage(Workbook workbook, Sheet sheet, String nameString, Collection data) {
//数据在第几行
final int size = workbook.getAllNames().size();
int columnIndex = 0;
String format = "%s!$%s$%s:$%s$%s";
Name name = workbook.createName();
name.setNameName(nameString);
String cellColumnFlag = getCellColumnFlag(columnIndex + 2);
int nameManageRegan = CollectionUtils.isEmpty(data) ? 1 : data.size() + 1;
String nameManageScope = String.format(format, sheet.getSheetName(), cellColumnFlag, size + 1, getCellColumnFlag(nameManageRegan), size + 1);
name.setRefersToFormula(nameManageScope);
createCell(1, sheet, size, columnIndex, nameString);
if (CollectionUtils.isNotEmpty(data)) {
for (Object val : data) {
createCell(1, sheet, size, ++columnIndex, String.valueOf(val));
}
}
return size;
}
private static String getCellColumnFlag(int num) {
String colFiled = "";
int chuNum = 0;
int yuNum = 0;
if (num >= 1 && num <= 26) {
colFiled = doHandle(num);
} else {
chuNum = num / 26;
yuNum = num % 26;
yuNum = yuNum == 0 ? 1 : yuNum;
colFiled += doHandle(chuNum);
colFiled += doHandle(yuNum);
}
return colFiled;
}
private static String doHandle(int num) {
return String.valueOf((char) (num + 64));
}
private static void createCell(int type, Sheet sheet, int rowIndex, int colIndex, Object val) {
Row row = sheet.getRow(rowIndex);
if (row == null) {
row = sheet.createRow(rowIndex);
}
Cell cell = row.getCell(colIndex);
if (cell == null) {
cell = row.createCell(colIndex);
}
if (type == 0) {
cell.setCellStyle(styles.get("header"));
row.setHeightInPoints(40);
}
cell.setCellValue(val == null ? "" : val.toString());
}
private static Map<String, CellStyle> createStyles(Workbook wb) {
// 标题
Map<String, CellStyle> styles = new HashMap<String, CellStyle>();
CellStyle style = wb.createCellStyle();
style.setAlignment(HorizontalAlignment.CENTER);
style.setVerticalAlignment(VerticalAlignment.CENTER);
Font titleFont = wb.createFont();
titleFont.setFontName("Arial");
titleFont.setFontHeightInPoints((short) 16);
titleFont.setBold(true);
style.setFont(titleFont);
style.setWrapText(true);
styles.put("title", style);
// 表头
style = wb.createCellStyle();
style.setAlignment(HorizontalAlignment.CENTER);
style.setVerticalAlignment(VerticalAlignment.CENTER);
style.setBorderRight(BorderStyle.THIN);
style.setRightBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
style.setBorderLeft(BorderStyle.THIN);
style.setLeftBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
style.setBorderTop(BorderStyle.THIN);
style.setTopBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
style.setBorderBottom(BorderStyle.THIN);
style.setBottomBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
style.setFillForegroundColor(IndexedColors.GREY_50_PERCENT.getIndex());
style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
Font headerFont = wb.createFont();
headerFont.setFontName("Arial");
headerFont.setFontHeightInPoints((short) 10);
headerFont.setBold(true);
headerFont.setColor(IndexedColors.WHITE.getIndex());
style.setFont(headerFont);
style.setWrapText(true);//换行
styles.put("header", style);
return styles;
}
private static class NameMapping {
private String fieldName;
private Excel excel;
public NameMapping(String fieldName, Excel excel) {
this.fieldName = fieldName;
this.excel = excel;
}
public String getFieldName() {
return fieldName;
}
public void setFieldName(String fieldName) {
this.fieldName = fieldName;
}
public Excel getExcel() {
return excel;
}
public void setExcel(Excel excel) {
this.excel = excel;
}
}
}
6)反射类 ReflectionUtil
package com.gongl.utils;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author gongl
* @date 2022-08-16
*/
public class ReflectionUtil {
public static Map<String, Field> getFieldMap(Object object) {
Map<String, Field> fieldMap = new ConcurrentHashMap<>();
refReflectionField(object, fieldMap);
return fieldMap;
}
private static void refReflectionField(Object object, Map<String, Field> fieldMap) {
Field[] fields = object.getClass().getDeclaredFields();
for (Field field : fields) {
fieldMap.put(field.getName(), field);
}
Class<?> superclass = object.getClass().getSuperclass();
if (superclass != null && !"java.lang.Object".equals(superclass.getName())) {
refReflectionField(superclass, fieldMap);
}
}
public static Object getMethod(String methodFullName, Object obj, Map<String, Object> map) throws InvocationTargetException, IllegalAccessException {
int lastIndex = methodFullName.lastIndexOf('.');
String className = methodFullName.substring(0, lastIndex);
String methodName = methodFullName.substring(lastIndex + 1);
try {
Class<?> clazz = Class.forName(className);
Method[] methods = clazz.getMethods();
for (Method methodObject : methods) {
if (methodObject.getName().equals(methodName)) {
return methodObject.invoke(obj, map);
}
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return null;
}
}
7) 最后写个测试类测试
package com.gongl;
import com.gongl.base.TestTemplate;
import com.gongl.utils.ExcelUtils;
import org.junit.Test;
import java.io.IOException;
public class AppTest {
@Test
public void test() throws IOException {
//测试 //直接下载文件到本地
ExcelUtils.createExcel(new TestTemplate(),"测试模板下载",null,null);
}
}
经过上面一顿操作后,最后呈现的就是步骤2)中的结果了。如果有更好的方式,希望大家指教