preface
This is the fifth day of my participation in the November Gwen Challenge. Check out the details: The last Gwen Challenge 2021. Not much to say, to say is to fill the hole left by the previous article [ExcelUtil] implementation file write to the client download process – nuggets (juejin. Cn).
Demand analysis
In addition to the most basic header name column widths, headers and content adaptive in China and foreign countries, still need to increase the position of the specified sequence of header, specify the date of export data time date format, motor, motor for the enumeration content want to be able to read with a specified delimiter writing values, in addition, for countless according to cell can according to the demand to the default value…
Finally, give a flag whether or not to export data to meet the need: sometimes we need to export a template, which is required for the title but requires the user to fill in the content manually.
I:
Code implementation
Custom annotations
First, customize an annotation as required, where each attribute corresponds to a function:
/ * * *@description: Custom export Excel data annotations *@author: HUALEI
* @date: 2021-11-19
* @time: and * /
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Excel {
/** * Export to Excel header alias */
String headerAlias(a) default "";
/** * Export in Excel */
int sort(a) default Integer.MAX_VALUE;
/** * The date format is YYYY-MM-DD */
String dateFormat(a) default "";
/** * reads the content based on the delimiter expression (e.g., 0= male,1= female,2= unknown) */
String readConverterExp(a) default "";
/** * delimiter (default is "," comma), reads the contents of the string group (note: some special delimiter characters need to be escaped with "\\sparator" or" [sparator]", otherwise the delimiter will fail) */
String separator(a) default ",";
/** * The default value of the field */ when the value is null
String defaultValue(a) default "";
/** * Whether to export data */
boolean isExport(a) default true;
enum Type {
/** Export import */
ALL(0),
/** only */ is exported
EXPORT(1),
/** Imports only */
IMPORT(2);
private final int value;
Type(int value) {
this.value = value;
}
public int value(a) {
return this.value; }}/** * field type (0: export import; 1: export only. 2: Import only) */
Type type(a) default Type.ALL;
}
Copy the code
The annotation has an internal enumeration class of Type to distinguish between an import and an export of the annotated identified fields, although the requirements here are just exported to help you stay on top of the requirements.
Toolclass encapsulation
Through the new ExcelUtil < > (XXX. Class); The ExcelUtil
class contains the file name, worksheet name and other basic attributes. The annotation Field list is used to store the annotation object identified by the annotation through reflection and the corresponding annotation properties. The internal storage structure is: [[Field, Excel],… .
/ * * *@description: ExcelUtil tool class secondary encapsulation *@author: HUALEI
* @date: 2021-11-20
* @time: inquire * /
public class ExcelUtil<T> {
private static final Logger logger = LoggerFactory.getLogger(ExcelUtil.class);
/** * Excel file name */
private String fileName;
/** * Worksheet name */
private String sheetName;
/** * Export type */
private Excel.Type type;
/** * File name suffix */
private String fileNameSuffix;
/** * Import/export data source list */
private List<T> sourceList;
/** * Annotated Field list [[Field, Excel]...] * /
private List<Object[]> fields;
/** * entity object */
public Class<T> clazz;
/** * Excel writer */
public ExcelWriter excelWriter;
public ExcelUtil(Class<T> clazz) {
this.clazz = clazz; }... . }Copy the code
In addition to member variables in the encapsulation class, the most important is the member method. Considering that the exported file may sometimes need.xls format, so I override the export Excel method, the default is.xlsx format.
/** * Write the data source list to an Excel file **@paramResponse HttpServletResponse object *@paramList Data source list *@paramFileName Excel fileName *@paramSheetName sheetName in Excel */
public void exportExcel(HttpServletResponse response, List
list, String fileName, String sheetName )
throws Exception {
this.excelWriter = cn.hutool.poi.excel.ExcelUtil.getBigWriter();
logger.info("=============== initialize Excel ===============");
init(list, fileName, sheetName, Excel.Type.EXPORT);
exportExcel(response, null);
logger.info("=============== Excel export success ===============");
}
/** * Write the data source list to an Excel file **@paramResponse HttpServletResponse object *@paramList Data source list *@paramFileName Excel fileName *@paramFileNameSuffix Excel fileNameSuffix *@paramSheetName sheetName in Excel */
public void exportExcel(HttpServletResponse response, List
list, String fileName, String fileNameSuffix, String sheetName )
throws Exception {
this.excelWriter = cn.hutool.poi.excel.ExcelUtil.getBigWriter();
logger.info("=============== initialize Excel ===============");
init(list, fileName, sheetName, Excel.Type.EXPORT);
exportExcel(response, fileNameSuffix);
logger.info("=============== Excel export success ===============");
}
Copy the code
In the export method, we initialize the writer first, and then initialize the class property value:
/** * Initializes the class attribute **@paramList Data source list *@paramFileName Specifies the name of the export file *@paramSheetName sheetName *@paramType Export type */
public void init(List<T> list, String fileName, String sheetName, Excel.Type type) throws Exception {
this.sourceList = Optional.ofNullable(list).orElseGet(ArrayList<T>::new);
this.fileName = fileName;
this.sheetName = sheetName;
// Set the Sheet name
this.excelWriter.renameSheet(sheetName);
this.type = type;
// Create the header
createExcelField();
// Process the data source
handleDataSource();
}
Copy the code
After initializing the partial member variable, create the specified sequence header and set the header alias:
/** * creates the specified sequence header and sets the header alias */
private void createExcelField(a) {
this.fields = new ArrayList<Object[]>();
// Temporarily store variables
List<Field> tempFields = new ArrayList<>();
// Get the list of all declared fields of the target entity object and place them in temporary storage variables
tempFields.addAll(Arrays.asList(clazz.getSuperclass().getDeclaredFields()));
tempFields.addAll(Arrays.asList(clazz.getDeclaredFields()));
// Filter out the fields marked by @excel from the list of declared fields
tempFields.stream()
.filter(field -> field.isAnnotationPresent(Excel.class))
.forEach(field -> {
// Get the annotation property object
Excel attr = field.getAnnotation(Excel.class);
// Filter the target export type
if(attr ! =null && (attr.type() == Excel.Type.ALL || attr.type() == this.type)) {
// Populate the list of annotations [[Field, Excel]]
this.fields.add(newObject[]{ field, attr }); }});// Sort in ascending order by the value of the sort attribute in the annotation
this.fields.stream()
.sorted(Comparator.comparing( arr -> ((Excel) arr[1]).sort() ))
.collect(Collectors.toList())
// Set the table header aliases in sequence
.forEach(arr -> {
String fieldName = ((Field) arr[0]).getName();
Excel attr = (Excel) arr[1];
this.excelWriter.addHeaderAlias(fieldName, StrUtil.isBlank(attr.headerAlias()) ? fieldName : attr.headerAlias());
});
}
Copy the code
Get the parent class of the target entity object and all of its declaration fields, store them in a temporary list of fields, then loop through and filter out the fields identified by the @excel annotation, and then build an array of size 2 by filtering the target export type into the annotation field list this.fields.
Second, sort in ascending order by the value of the sort attribute in the annotation, or by default by the order in which the field is defined if no order value is set. After sorting, set the table header aliases in sequence. If not, keep the default field names.
Once the header is created, the next step is to process the data source list according to the annotation property object on each field in the annotation field list Fields:
/** * Process the data source list according to the annotation properties **@throwsException gets the Exception that a class attribute value may throw */
private void handleDataSource(a) throws Exception {
for (Object[] arr : this.fields) {
// Annotate identified fields
Field field = (Field) arr[0];
// Annotate the property object
Excel attr = (Excel) arr[1];
// Make the entity class private property accessible
field.setAccessible(true);
for (T object: this.sourceList) {
// Get the property value of the current field
Object value = field.get(object);
if (attr.isExport()) {
if(value ! =null) {
// Set the time format
if (StrUtil.isNotBlank(attr.dateFormat())) {
field.set(object, cn.hutool.core.convert.Convert.convert(field.getType(), DateUtil.format(new DateTime(value.toString()), attr.dateFormat())));
}
// Set the conversion value
if(StrUtil.isNotBlank(attr.readConverterExp())) { String convertResult = convertByExp(Convert.toStr(value), attr.readConverterExp(), attr.separator()); field.set(object, convertResult); }}else {
// Set the default value
if(StrUtil.isNotBlank(attr.defaultValue())) { field.set(object, attr.defaultValue()); }}}else {
field.set(object, null); }}}}Copy the code
The above code mainly obtains the property value of the field field under the current object through Java reflection principle, and determines whether the current column data needs to be exported. If necessary, it further determines whether the corresponding value of the attribute in the annotation has a value. If there is a value and the field attribute value is not null, it changes the original value. Values that have a field attribute value of NULL can be set to the specified default value. Otherwise, no export is required, all cells of the column are empty.
Simple understanding of the text may not be as intuitive and clear as a flow chart, which gives you an arrangement:
For the method convertByExp() to parse the exported value, split the translated annotation string by delimiter, and parse it according to the principle that the left side of the “=” equals sign is the key and the right side is the value. The specific code is as follows:
/** * Parse the exported value **@paramPropertyValue Parameter value *@paramConverterExp translation notes *@paramSeparator Separator *@returnParsed value */
public static String convertByExp(String propertyValue, String converterExp, String separator) {
StringBuilder propertyString = new StringBuilder();
String[] convertSource = converterExp.split(separator);
for (String item : convertSource) {
String[] itemArray = item.split("=");
if (StringUtils.containsAny(separator, propertyValue)) {
for (String value : propertyValue.split(separator)) {
if (itemArray[0].equals(value)) {
propertyString.append(itemArray[1]).append(separator);
break; }}}else {
if (itemArray[0].equals(propertyValue)) {
return itemArray[1]; }}}return StringUtils.stripEnd(propertyString.toString(), separator);
}
Copy the code
Now that all the initialization is done, you can happily write data into Excel and finally write files to the client for download.
/** * write to the client to download **@paramResponse HttpServletResponse object *@paramSuffix Export Excel file name suffix */
public void exportExcel(HttpServletResponse response, String suffix) throws IOException {
/ / the output stream
ServletOutputStream out = response.getOutputStream();
this.excelWriter.write(this.sourceList, true);
cellWidthSelfAdaption();
initResponse(response, suffix);
this.excelWriter.flush(out, true);
// Close Writer to release memory
this.excelWriter.close();
// Close the output Servlet stream
IoUtil.close(out);
}
Copy the code
-
The cellWidthSelfAdaption() method is used for the Chinese width adaptation, the code will not be pasted here, and the code will be better understood, please click here and the portal will be better understood
-
InitResponse () initializes the HttpServletResponse object based on the exported Excel filename suffix for the response body and response type.
/** * Initializes the HttpServletResponse object ** based on the exported Excel filename suffix@paramResponse HttpServletResponse object *@paramSuffix File name suffix *@throwsUnsupportedEncodingException doesn't support coding abnormal * /
public void initResponse(HttpServletResponse response, String suffix) throws UnsupportedEncodingException {
// The default export file name suffix
this.fileNameSuffix = ".xlsx";
if(suffix ! =null) {
switch (suffix.toLowerCase()) {
case "xls":
case ".xls":
this.fileNameSuffix = ".xls";
response.setContentType("application/vnd.ms-excel; charset=utf-8");
break;
case "xlsx":
case ".xlsx":
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet; charset=utf-8");
break;
default:
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet; charset=utf-8"); }}else {
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet; charset=utf-8");
}
// The file name is in Chinese
String encodingFilename = encodingFilename(this.fileName);
response.setHeader("Content-Disposition"."attachment; filename="+ encodingFilename);
}
Copy the code
The default export file format is.xlsx, but it can also be specified as.xls by setting different content types. As for the exported file name, add a suffix and a code to the response head.
/** * encoding file name **@paramFilename indicates the filename */
public String encodingFilename(String filename) throws UnsupportedEncodingException {
filename = filename + this.fileNameSuffix;
return URLEncoder.encode(filename, CharsetUtil.UTF_8);
}
Copy the code
At this point, the code for the annotation + ExcelUtil secondary wrapping is complete.
Expose interfaces
Entity objects
As usual, entity objects give it the @Excel annotation and add a property Buff:
@Data
public class ProvinceCustomAnnotationExcelVO implements Serializable {
private static final long serialVersionUID = 877981781678377000L;
/** ** province */
@excel (headerAlias = "province ")
private String province;
/** ** province */
@excel (headerAlias = "for short ")
private String abbr;
/** * The area of the province (km²) */
@excel (headerAlias = "area (km²) ")
private Integer area;
/ Population of ** * province (ten thousand) */
@excel (headerAlias = "Population (ten thousand) ")
private BigDecimal population;
/** ** ** ** ** ** ** ** *
@excel (headerAlias = "Famous spot ")
private String attraction;
/** * The postcode of the provincial capital */
@ Excel (headerAlias = "postal code", readConverterExp = 100 = "cow force is finished | = 050000 ha ha ha", the separator = "\ \ |")
private String postcode;
/** * The name of the provincial capital */
@excel (headerAlias = "default ", defaultValue =" default ")
private String city;
/** ** The provincial capital's alias */
@excel (headerAlias = "alias ", isExport = false)
private String nickname;
/** ** Climate type of provincial capitals */
@excel (headerAlias = "Climate type ")
private String climate;
/** ** ** */
@excel (headerAlias = "license number ", defaultValue =" data not yet available ")
private String carcode;
/** * test time */
@excel (headerAlias = "create time ", dateFormat = "yyyy ")
private String createTime;
}
Copy the code
Control layer
Service layer getAllProvinceDetails() method code implementation please refer to [ExcelUtil] implementation file write to the client download process – dig gold (juejin. Cn).
@GetMapping("provinces/custom/excel/export/{fileNameSuffix}")
public void customAnnotationExcelExport(HttpServletResponse response, @PathVariable("fileNameSuffix") String fileNameSuffix) throws Exception {
// Get the province details
List<ProvinceExcelVO> provinceExcelList = this.provinceService.getAllProvinceDetails();
// The Bean object is converted to get the data source list
List<ProvinceCustomAnnotationExcelVO> provinceCustomAnnotationExcelList = BeanUtil.copyToList(provinceExcelList, ProvinceCustomAnnotationExcelVO.class);
// To test export time formatting, add point random date time
provinceCustomAnnotationExcelList.forEach(p -> p.setCreateTime(RandomUtil.randomDate(new Date(), DateField.SECOND, 0.24*60*60).toString()));
// Create an ExcelUtil object with a parameter construct (required)
ExcelUtil<ProvinceCustomAnnotationExcelVO> excelUtil = new ExcelUtil<>(ProvinceCustomAnnotationExcelVO.class);
// File name (date of the day _ province information)
String fileName = StrUtil.format("{}{} province information", DateUtil.today(), StrUtil.UNDERLINE);
// Sheet Sheet name
String sheetName = "List of Provinces";
if (StrUtil.isBlank(fileNameSuffix)) {
// Test the export default format
excelUtil.exportExcel(response, provinceCustomAnnotationExcelList, fileName, sheetName);
} else {
// Tests the export format specifiedexcelUtil.exportExcel(response, provinceCustomAnnotationExcelList, fileName, fileNameSuffix, sheetName); }}Copy the code
The exported file name suffix is placed on the path mainly for testing convenience, which is not necessary in actual development for Duck!
The interface test
Start testing:
GET: http://localhost:8088/file/provinces/custom/excel/export/xls
GET: http://localhost:8088/file/provinces/custom/excel/export/.xlsx
GET: http://localhost:8088/file/provinces/custom/excel/export/""
GET: http://localhost:8088/file/provinces/custom/excel/export/HUALEI
All tests passed, can be called perfect, pit filling success!! ✿ Remember, (° °) Blue ✿
conclusion
The overall implementation is not too difficult, the use of annotations drive is not too sweet, it is very convenient to use, even if the small white did not learn programming will also use, one or two lines of code can complete a list of data sources export.
The only deficiency is that the data import is not integrated into, but the focus of this article is not import, ha ha ha, interested partners can try oh ヾ(◍°∇°◍) Ada
At the end
Writing is not easy, welcome everyone to like, comment, your attention, like is my unremitting power, thank you to see here! Peace and Love.