目录

目录

前言

1. 功能测试

1.1 测试准备

1.2 数据导入

1.2.1 导入解析为JSON

1.2.2 导入解析为对象(基础)

1.2.3 导入解析为对象(字段自动映射)

1.2.4 导入解析为对象(获取行号)

1.2.5 导入解析为对象(获取原始数据)

1.2.6 导入解析为对象(获取错误提示)

1.2.7 导入解析为对象(限制字段长度)

1.2.8 导入解析为对象(必填字段验证)

1.2.9 导入解析为对象(数据唯一性验证)

1.2.10 导入多Sheet页

1.3 数据导出

1.3.1 动态导出(基础)

1.3.2 动态导出(导出图片)

1.3.3 动态导出(实现下拉列表)

1.3.4 动态导出(横向合并)

1.3.5 动态导出(纵向合并)

1.3.6 导出模板(基础)

1.3.7 导出模板(附示例数据)

1.3.8 按对象导出(基础)

1.3.9 按对象导出(数据映射)

1.3.10 按对象导出(调整表头顺序)

1.3.11 多Sheet页导出

2. 环境准备

2.1 Maven 依赖

2.2 类文件

ExcelUtils

ExcelImport

ExcelExport

ExcelClassField

3. 更新优化

3.1 优化身份证导出显示为科学计数法的问题(2022-07-04)

4. 疑问解答

4.1 关于身份证导入显示科学计数法的问题(2022-07-04)


前言

最近抽了两天时间,把Java实现表格的相关操作进行了封装,本次封装是基于 POI 的二次开发,最终使用只需要调用一个工具类中的方法,就能满足业务中绝大部门的导入和导出需求。

1. 功能测试

1.1 测试准备

在做测试前,我们需要將【2. 环境准备】中的四个文件拷贝在工程里(如:我这里均放在了com.zyq.util.excel 包下)。

1.2 数据导入

1.2.1 导入解析为JSON

比如,我们有下面一个表格:

Controller 代码:

@PostMapping("/import")
public JSONArray importUser(@RequestPart("file")MultipartFile file) throws Exception {JSONArray array = ExcelUtils.readMultipartFile(file);System.out.println("导入数据为:" + array);return array;
}

测试效果:

1.2.2 导入解析为对象(基础)

首先,你需要创建一个与导入表格对应的Java实体对象,并打上对应的Excel解析的导入注解,@ExcelImport注解的value则为表头名称。

Controller 代码:

@PostMapping("/import")
public void importUser(@RequestPart("file")MultipartFile file) throws Exception {List<User> users = ExcelUtils.readMultipartFile(file, User.class);for (User user : users) {System.out.println(user.toString());}
}

测试效果:

1.2.3 导入解析为对象(字段自动映射)

对于有的枚举数据,通常我们导入的时候,表格中的数据是值,而在数据保存时,往往用的是键,比如:我们用sex=1可以表示为男,sex=2表示为女,那么我们通过配置也可以达到导入时,数据的自动映射。

那么,我们只需要将Java实体中的对象sex字段的类型改为对应的数字类型Integer,然后再注解中配置好 kv 属性(属性格式为:键1-值1;键2-值2;键3-值3;.....)

Cotroller 代码略(和 1.2.2 完全一致)。

测试效果:可以看到已经自动映射成功了。

1.2.4 导入解析为对象(获取行号)

我们在做页面数据导入时,有时候可能需要获取行号,好追踪导入的数据。

那么,我们只需要在对应的实体中加入一个 int 类型的 rowNum 字段即可。

Cotroller 代码略(和 1.2.2 完全一致)。

测试效果:

1.2.5 导入解析为对象(获取原始数据)

在做页面数据导入的时候,如果某行存在错误,一般我们会将原始的数据拿出来分析,为什么会造成数据错误。那么,我们在实体类中,增加一个 String 类型的 rowData 字段即可。

Cotroller 代码略(和 1.2.2 完全一致)。

测试效果:

1.2.6 导入解析为对象(获取错误提示)

当我们在导入数据的时候,如果某行数据存在,字段类型不正确,长度超过最大限制(详见1.2.7),必填字段验证(1.2.8),数据唯一性验证(1.2.9)等一些错误时候,我们可以往对象中添加一个 String 类型的 rowTips 字段,则可以直接拿到对应的错误信息。

比如,我们将表格中赵子龙的性别改为F(F并不是映射数据),将大乔的性别改为二十八(不能转换为Integer类型数据)。

Cotroller 代码略(和 1.2.2 完全一致)。

测试效果:可以看到,我们可以通过 rowTips 直接拿到对应的错误数据提示。

1.2.7 导入解析为对象(限制字段长度)

比如,我们手机通常为11为长度,那么不妨限制电话的最大长度位数为11位。

对应的做法,就是在 @ExcelImport 注解中,设置 maxLength = 11 即可。

比如,我们将诸葛孔明的电话长度设置为超过11位数的一个字符串。

Cotroller 代码略(和 1.2.2 完全一致)。

测试效果:

1.2.8 导入解析为对象(必填字段验证)

我们在做数据导入的时候,往往还会有一些必填字段,比如用户的名称,电话。

那么,我们只需要在 @ExcelImport 注解属性中,加上 required = true 即可。

我们将诸葛孔明的电话,以及第4行的姓名去掉,进行测试。

Cotroller 代码略(和 1.2.2 完全一致)。

测试效果:

1.2.9 导入解析为对象(数据唯一性验证)

(1) 单字段唯一性验证

我们在导入数据的时候,某个字段是具有唯一性的,比如我们这里假设规定姓名不能重复,那么则可以在对应字段的 @ExcelImport 注解上加上 unique = true 属性。

这里我们构建2条姓名一样的数据进行测试。

Cotroller 代码略(和 1.2.2 完全一致)。

测试效果:

(2)多字段唯一性验证

如果你导入的数据存在多字段唯一性验证这种情况,只需要将每个对应字段的 @ExcelImport 注解属性中,都加上 unique = true 即可。

比如:我们将姓名和电话两个字段进行联合唯一性验证(即不能存在有名称和电话都一样的数据,单个字段属性重复允许)。

首先,我们将刚刚(1)的数据进行导入。

测试效果:可以看到,虽然名称有相同,但电话不相同,所以这里并没有提示唯一性验证错误。

现在,我们将最后一行的电话也改为和第1行一样的,于是,现在就存在了违背唯一性的两条数据。

测试效果:可以看到,我们的联合唯一性验证生效了。

1.2.10 导入多Sheet页

如果你的导入表格存在多个sheet页(如下图),并想解析每个sheet页的数据,那么也是可以的。

Controller 代码:

@PostMapping("/import")
public void upload(@RequestPart("file") MultipartFile file) throws Exception {Map<String, JSONArray> map = ExcelUtils.readFileManySheet(file);map.forEach((key, value) -> {System.out.println("Sheet名称:" + key);System.out.println("Sheet数据:" + value);System.out.println("----------------------");});
}

代码截图:

测试效果:

1.3 数据导出

1.3.1 动态导出(基础)

这种方式十分灵活,表中的数据,完全自定义设置。

Controller 代码:

@GetMapping("/export")
public void export(HttpServletResponse response) {// 表头数据List<Object> head = Arrays.asList("姓名","年龄","性别","头像");// 用户1数据List<Object> user1 = new ArrayList<>();user1.add("诸葛亮");user1.add(60);user1.add("男");user1.add("https://profile.csdnimg.cn/A/7/3/3_sunnyzyq");// 用户2数据List<Object> user2 = new ArrayList<>();user2.add("大乔");user2.add(28);user2.add("女");user2.add("https://profile.csdnimg.cn/6/1/9/0_m0_48717371");// 将数据汇总List<List<Object>> sheetDataList = new ArrayList<>();sheetDataList.add(head);sheetDataList.add(user1);sheetDataList.add(user2);// 导出数据ExcelUtils.export(response,"用户表", sheetDataList);
}

代码截图:

由于是 get 请求,我们直接在浏览器上输入请求地址即可触发下载。

打开下载表格,我们可以看到,表中的数据和我们代码组装的顺序一致。

1.3.2 动态导出(导出图片)

如果你的导出中,需要将对应图片链接直接显示为图片的话,那么,这里也是可以的,只需要将对应的类型转为 java.net.URL 类型即可(注意:转的时候有异常处理,为了方便演示,我这里直接抛出)

测试效果:

1.3.3 动态导出(实现下拉列表)

我们在做一些数据导出的时候,可能要对某一行的下拉数据进行约束限制。

比如,当我们下载一个导入模版的时候,我们可以将性别,城市对应的列设置为下拉选择。

测试效果:

1.3.4 动态导出(横向合并)

比如,我们将表头横向合并,只需要将合并的单元格设置为 ExcelUtils.COLUMN_MERGE 即可。

测试效果:可以看到表头的地址已经被合并了。

1.3.5 动态导出(纵向合并)

除了横向合并,我们还可以进行纵向合并,只需要将合并的单元格设置为 ExcelUtils.ROW_MERGE 即可。

测试效果:

1.3.6 导出模板(基础)

我们在做数据导入的时候,往往首先会提供一个模版供其下载,这样用户在导入的时候才知道如何去填写数据。导出模板除了可以用上面的动态导出,这里还提供了一种更加便捷的写法。只需要创建一个类,然后再对应字段上打上 @ExcelExport 注解类即可。

Controller 代码:

@GetMapping("/export")
public void export(HttpServletResponse response) {ExcelUtils.exportTemplate(response, "用户表", User.class);
}

代码截图:

测试效果:

1.3.7 导出模板(附示例数据)

我们在做模版下载时候,有时往往会携带一条样本数据,好提示用户数据格式是什么,那么我们只需要在对应字段上进行配置即可。

Controller代码:

测试效果:

1.3.8 按对象导出(基础)

我们还可以通过 List 对象,对数据直接进行导出。首先,同样需要在对应类的字段上,设置导出名称。

Controller 代码:

测试效果:

1.3.9 按对象导出(数据映射)

在上面 1.3.8 的导出中,我们可以看到,性别数据导出来是1和2,这个不利于用户体验,应该需要转换为对应的中文,我们可以在字段注解上进行对应的配置。

Controller 代码略(和1.3.8完全一致)

测试效果:可以看到1和2显示为了对应的男和女

1.3.10 按对象导出(调整表头顺序)

如果你需要对表头字段进行排序,有两种方式:

第一种:按照表格的顺序,排列Java类中的字段;

第二种:在 @ExcelExport 注解中,指定 sort 属性,其值越少,排名越靠前。

Controller 代码略(和1.3.8完全一致)

测试效果:可以看到,此时导出数据的表头顺序,和我们指定的顺序完全一致。

1.3.11 多Sheet页导出

Controller 代码(示例):

    /*** 导出多 Sheet 页实现*/@GetMapping("/exportManySheet")public void exportManySheet(HttpServletResponse response) {// 第 1 个 Sheet 页List<List<Object>> sheet1 = new ArrayList<>();List<Object> sheet1Head = new ArrayList<>();sheet1Head.add("姓名");sheet1Head.add("数学");sheet1Head.add("英语");sheet1.add(sheet1Head);List<Object> row1 = new ArrayList<>();row1.add("Jack");row1.add(85);row1.add(100);sheet1.add(row1);List<Object> row2 = new ArrayList<>();row2.add("Marry");row2.add(85);row2.add(100);sheet1.add(row2);// 第 2 个 Sheet 页List<List<Object>> sheet2 = new ArrayList<>();List<Object> sheet2Head = new ArrayList<>();sheet2Head.add("姓名");sheet2Head.add("音乐");sheet2Head.add("美术");sheet2.add(sheet2Head);List<Object> row01 = new ArrayList<>();row01.add("Jack");row01.add(77);row01.add(66);sheet2.add(row01);List<Object> row02 = new ArrayList<>();row02.add("Marry");row02.add(99);row02.add(88);sheet2.add(row02);// 将两个 Sheet 页添加到集合中Map<String, List<List<Object>>> sheets = new LinkedHashMap<>();sheets.put("文化课", sheet1);sheets.put("艺术课", sheet2);// 导出数据ExcelUtils.exportManySheet(response, "学生成绩表", sheets);}

测试效果:

第一个 Sheet 页

第二个 Sheet 页

2. 环境准备

2.1 Maven 依赖

本次工具类的封装主要依赖于阿里巴巴的JSON包,以及表格处理的POI包,所以我们需要导入这两个库的依赖包,另外,我们还需要文件上传的相关包,毕竟我们在浏览器页面,做Excel导入时,是上传的Excel文件。

<!-- 文件上传 -->
<dependency><groupId>org.apache.httpcomponents</groupId><artifactId>httpmime</artifactId><version>4.5.7</version>
</dependency>
<!-- JSON -->
<dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.41</version>
</dependency>
<!-- POI -->
<dependency><groupId>org.apache.poi</groupId><artifactId>poi-ooxml</artifactId><version>3.16</version>
</dependency>

2.2 类文件

ExcelUtils

package com.sonar.data.utils.excel;import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import org.apache.poi.hssf.usermodel.HSSFDataValidation;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.poifs.filesystem.POIFSFileSystem;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.usermodel.ClientAnchor.AnchorType;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.ss.util.CellRangeAddressList;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import org.apache.poi.xssf.usermodel.XSSFClientAnchor;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.web.multipart.MultipartFile;import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.net.URL;
import java.text.NumberFormat;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.Map.Entry;
import java.util.regex.Pattern;/*** Excel导入导出工具类* 原文链接(不定时增加新功能): https://zyqok.blog.csdn.net/article/details/121994504** @author sunnyzyq* @date 2021/12/17*/
@SuppressWarnings("unused")
public class ExcelUtils {private static final String XLSX = ".xlsx";private static final String XLS = ".xls";public static final String ROW_MERGE = "row_merge";public static final String COLUMN_MERGE = "column_merge";private static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";private static final String ROW_NUM = "rowNum";private static final String ROW_DATA = "rowData";private static final String ROW_TIPS = "rowTips";private static final int CELL_OTHER = 0;private static final int CELL_ROW_MERGE = 1;private static final int CELL_COLUMN_MERGE = 2;private static final int IMG_HEIGHT = 30;private static final int IMG_WIDTH = 30;private static final char LEAN_LINE = '/';private static final int BYTES_DEFAULT_LENGTH = 10240;private static final NumberFormat NUMBER_FORMAT = NumberFormat.getNumberInstance();public static <T> List<T> readFile(File file, Class<T> clazz) throws Exception {JSONArray array = readFile(file);return getBeanList(array, clazz);}public static <T> List<T> readMultipartFile(MultipartFile mFile, Class<T> clazz) throws Exception {JSONArray array = readMultipartFile(mFile);return getBeanList(array, clazz);}public static JSONArray readFile(File file) throws Exception {return readExcel(null, file);}public static JSONArray readMultipartFile(MultipartFile mFile) throws Exception {return readExcel(mFile, null);}public static Map<String, JSONArray> readFileManySheet(File file) throws Exception {return readExcelManySheet(null, file);}public static Map<String, JSONArray> readFileManySheet(MultipartFile file) throws Exception {return readExcelManySheet(file, null);}private static <T> List<T> getBeanList(JSONArray array, Class<T> clazz) throws Exception {List<T> list = new ArrayList<>();Map<Integer, String> uniqueMap = new HashMap<>(16);for (int i = 0; i < array.size(); i++) {list.add(getBean(clazz, array.getJSONObject(i), uniqueMap));}return list;}/*** 获取每个对象的数据*/private static <T> T getBean(Class<T> c, JSONObject obj, Map<Integer, String> uniqueMap) throws Exception {T t = c.newInstance();Field[] fields = c.getDeclaredFields();List<String> errMsgList = new ArrayList<>();boolean hasRowTipsField = false;StringBuilder uniqueBuilder = new StringBuilder();int rowNum = 0;for (Field field : fields) {// 行号if (field.getName().equals(ROW_NUM)) {rowNum = obj.getInteger(ROW_NUM);field.setAccessible(true);field.set(t, rowNum);continue;}// 是否需要设置异常信息if (field.getName().equals(ROW_TIPS)) {hasRowTipsField = true;continue;}// 原始数据if (field.getName().equals(ROW_DATA)) {field.setAccessible(true);field.set(t, obj.toString());continue;}// 设置对应属性值setFieldValue(t, field, obj, uniqueBuilder, errMsgList);}// 数据唯一性校验if (uniqueBuilder.length() > 0) {if (uniqueMap.containsValue(uniqueBuilder.toString())) {Set<Integer> rowNumKeys = uniqueMap.keySet();for (Integer num : rowNumKeys) {if (uniqueMap.get(num).equals(uniqueBuilder.toString())) {errMsgList.add(String.format("数据唯一性校验失败,(%s)与第%s行重复)", uniqueBuilder, num));}}} else {uniqueMap.put(rowNum, uniqueBuilder.toString());}}// 失败处理if (errMsgList.isEmpty() && !hasRowTipsField) {return t;}StringBuilder sb = new StringBuilder();int size = errMsgList.size();for (int i = 0; i < size; i++) {if (i == size - 1) {sb.append(errMsgList.get(i));} else {sb.append(errMsgList.get(i)).append(";");}}// 设置错误信息for (Field field : fields) {if (field.getName().equals(ROW_TIPS)) {field.setAccessible(true);field.set(t, sb.toString());}}return t;}private static <T> void setFieldValue(T t, Field field, JSONObject obj, StringBuilder uniqueBuilder, List<String> errMsgList) {// 获取 ExcelImport 注解属性ExcelImport annotation = field.getAnnotation(ExcelImport.class);if (annotation == null) {return;}String cname = annotation.value();if (cname.trim().length() == 0) {return;}// 获取具体值String val = null;if (obj.containsKey(cname)) {val = getString(obj.getString(cname));}if (val == null) {return;}field.setAccessible(true);// 判断是否必填boolean require = annotation.required();if (require && val.isEmpty()) {errMsgList.add(String.format("[%s]不能为空", cname));return;}// 数据唯一性获取boolean unique = annotation.unique();if (unique) {if (uniqueBuilder.length() > 0) {uniqueBuilder.append("--").append(val);} else {uniqueBuilder.append(val);}}// 判断是否超过最大长度int maxLength = annotation.maxLength();if (maxLength > 0 && val.length() > maxLength) {errMsgList.add(String.format("[%s]长度不能超过%s个字符(当前%s个字符)", cname, maxLength, val.length()));}// 判断当前属性是否有映射关系LinkedHashMap<String, String> kvMap = getKvMap(annotation.kv());if (!kvMap.isEmpty()) {boolean isMatch = false;for (String key : kvMap.keySet()) {if (kvMap.get(key).equals(val)) {val = key;isMatch = true;break;}}if (!isMatch) {errMsgList.add(String.format("[%s]的值不正确(当前值为%s)", cname, val));return;}}// 其余情况根据类型赋值String fieldClassName = field.getType().getSimpleName();try {if ("String".equalsIgnoreCase(fieldClassName)) {field.set(t, val);} else if ("boolean".equalsIgnoreCase(fieldClassName)) {field.set(t, Boolean.valueOf(val));} else if ("int".equalsIgnoreCase(fieldClassName) || "Integer".equals(fieldClassName)) {try {field.set(t, Integer.valueOf(val));} catch (NumberFormatException e) {errMsgList.add(String.format("[%s]的值格式不正确(当前值为%s)", cname, val));}} else if ("double".equalsIgnoreCase(fieldClassName)) {field.set(t, Double.valueOf(val));} else if ("long".equalsIgnoreCase(fieldClassName)) {field.set(t, Long.valueOf(val));} else if ("BigDecimal".equalsIgnoreCase(fieldClassName)) {field.set(t, new BigDecimal(val));} else if ("Date".equalsIgnoreCase(fieldClassName)) {try {field.set(t, new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(val));} catch (Exception e) {field.set(t, new SimpleDateFormat("yyyy-MM-dd").parse(val));}}} catch (Exception e) {e.printStackTrace();}}private static Map<String, JSONArray> readExcelManySheet(MultipartFile mFile, File file) throws IOException {Workbook book = getWorkbook(mFile, file);if (book == null) {return Collections.emptyMap();}Map<String, JSONArray> map = new LinkedHashMap<>();for (int i = 0; i < book.getNumberOfSheets(); i++) {Sheet sheet = book.getSheetAt(i);JSONArray arr = readSheet(sheet);map.put(sheet.getSheetName(), arr);}book.close();return map;}private static JSONArray readExcel(MultipartFile mFile, File file) throws IOException {Workbook book = getWorkbook(mFile, file);if (book == null) {return new JSONArray();}JSONArray array = readSheet(book.getSheetAt(0));book.close();return array;}private static Workbook getWorkbook(MultipartFile mFile, File file) throws IOException {boolean fileNotExist = (file == null || !file.exists());if (mFile == null && fileNotExist) {return null;}// 解析表格数据InputStream in;String fileName;if (mFile != null) {// 上传文件解析in = mFile.getInputStream();fileName = getString(mFile.getOriginalFilename()).toLowerCase();} else {// 本地文件解析in = new FileInputStream(file);fileName = file.getName().toLowerCase();}Workbook book;if (fileName.endsWith(XLSX)) {book = new XSSFWorkbook(in);} else if (fileName.endsWith(XLS)) {POIFSFileSystem poifsFileSystem = new POIFSFileSystem(in);book = new HSSFWorkbook(poifsFileSystem);} else {return null;}in.close();return book;}private static JSONArray readSheet(Sheet sheet) {// 首行下标int rowStart = sheet.getFirstRowNum();// 尾行下标int rowEnd = sheet.getLastRowNum();// 获取表头行Row headRow = sheet.getRow(rowStart);if (headRow == null) {return new JSONArray();}int cellStart = headRow.getFirstCellNum();int cellEnd = headRow.getLastCellNum();Map<Integer, String> keyMap = new HashMap<>();for (int j = cellStart; j < cellEnd; j++) {// 获取表头数据String val = getCellValue(headRow.getCell(j));if (val != null && val.trim().length() != 0) {keyMap.put(j, val);}}// 如果表头没有数据则不进行解析if (keyMap.isEmpty()) {return (JSONArray) Collections.emptyList();}// 获取每行JSON对象的值JSONArray array = new JSONArray();// 如果首行与尾行相同,表明只有一行,返回表头数据if (rowStart == rowEnd) {JSONObject obj = new JSONObject();// 添加行号obj.put(ROW_NUM, 1);for (int i : keyMap.keySet()) {obj.put(keyMap.get(i), "");}array.add(obj);return array;}for (int i = rowStart + 1; i <= rowEnd; i++) {Row eachRow = sheet.getRow(i);JSONObject obj = new JSONObject();// 添加行号obj.put(ROW_NUM, i + 1);StringBuilder sb = new StringBuilder();for (int k = cellStart; k < cellEnd; k++) {if (eachRow != null) {String val = getCellValue(eachRow.getCell(k));// 所有数据添加到里面,用于判断该行是否为空sb.append(val);obj.put(keyMap.get(k), val);}}if (sb.length() > 0) {array.add(obj);}}return array;}private static String getCellValue(Cell cell) {// 空白或空if (cell == null || cell.getCellTypeEnum() == CellType.BLANK) {return "";}// String类型if (cell.getCellTypeEnum() == CellType.STRING) {String val = cell.getStringCellValue();if (val == null || val.trim().length() == 0) {return "";}return val.trim();}// 数字类型if (cell.getCellTypeEnum() == CellType.NUMERIC) {String s = cell.getNumericCellValue() + "";// 去掉尾巴上的小数点0if (Pattern.matches(".*\\.0*", s)) {return s.split("\\.")[0];} else {return s;}}// 布尔值类型if (cell.getCellTypeEnum() == CellType.BOOLEAN) {return cell.getBooleanCellValue() + "";}// 错误类型return cell.getCellFormula();}public static <T> void exportTemplate(HttpServletResponse response, String fileName, Class<T> clazz) {exportTemplate(response, fileName, fileName, clazz, false);}public static <T> void exportTemplate(HttpServletResponse response, String fileName, String sheetName,Class<T> clazz) {exportTemplate(response, fileName, sheetName, clazz, false);}public static <T> void exportTemplate(HttpServletResponse response, String fileName, Class<T> clazz,boolean isContainExample) {exportTemplate(response, fileName, fileName, clazz, isContainExample);}public static <T> void exportTemplate(HttpServletResponse response, String fileName, String sheetName,Class<T> clazz, boolean isContainExample) {// 获取表头字段List<ExcelClassField> headFieldList = getExcelClassFieldList(clazz);// 获取表头数据和示例数据List<List<Object>> sheetDataList = new ArrayList<>();List<Object> headList = new ArrayList<>();List<Object> exampleList = new ArrayList<>();Map<Integer, List<String>> selectMap = new LinkedHashMap<>();for (int i = 0; i < headFieldList.size(); i++) {ExcelClassField each = headFieldList.get(i);headList.add(each.getName());exampleList.add(each.getExample());LinkedHashMap<String, String> kvMap = each.getKvMap();if (kvMap != null && kvMap.size() > 0) {selectMap.put(i, new ArrayList<>(kvMap.values()));}}sheetDataList.add(headList);if (isContainExample) {sheetDataList.add(exampleList);}// 导出数据export(response, fileName, sheetName, sheetDataList, selectMap);}private static <T> List<ExcelClassField> getExcelClassFieldList(Class<T> clazz) {// 解析所有字段Field[] fields = clazz.getDeclaredFields();boolean hasExportAnnotation = false;Map<Integer, List<ExcelClassField>> map = new LinkedHashMap<>();List<Integer> sortList = new ArrayList<>();for (Field field : fields) {ExcelClassField cf = getExcelClassField(field);if (cf.getHasAnnotation() == 1) {hasExportAnnotation = true;}int sort = cf.getSort();if (map.containsKey(sort)) {map.get(sort).add(cf);} else {List<ExcelClassField> list = new ArrayList<>();list.add(cf);sortList.add(sort);map.put(sort, list);}}Collections.sort(sortList);// 获取表头List<ExcelClassField> headFieldList = new ArrayList<>();if (hasExportAnnotation) {for (Integer sort : sortList) {for (ExcelClassField cf : map.get(sort)) {if (cf.getHasAnnotation() == 1) {headFieldList.add(cf);}}}} else {headFieldList.addAll(map.get(0));}return headFieldList;}private static ExcelClassField getExcelClassField(Field field) {ExcelClassField cf = new ExcelClassField();String fieldName = field.getName();cf.setFieldName(fieldName);ExcelExport annotation = field.getAnnotation(ExcelExport.class);// 无 ExcelExport 注解情况if (annotation == null) {cf.setHasAnnotation(0);cf.setName(fieldName);cf.setSort(0);return cf;}// 有 ExcelExport 注解情况cf.setHasAnnotation(1);cf.setName(annotation.value());String example = getString(annotation.example());if (!example.isEmpty()) {if (isNumeric(example) && example.length() < 8) {cf.setExample(Double.valueOf(example));} else {cf.setExample(example);}} else {cf.setExample("");}cf.setSort(annotation.sort());// 解析映射String kv = getString(annotation.kv());cf.setKvMap(getKvMap(kv));return cf;}private static LinkedHashMap<String, String> getKvMap(String kv) {LinkedHashMap<String, String> kvMap = new LinkedHashMap<>();if (kv.isEmpty()) {return kvMap;}String[] kvs = kv.split(";");if (kvs.length == 0) {return kvMap;}for (String each : kvs) {String[] eachKv = getString(each).split("-");if (eachKv.length != 2) {continue;}String k = eachKv[0];String v = eachKv[1];if (k.isEmpty() || v.isEmpty()) {continue;}kvMap.put(k, v);}return kvMap;}/*** 导出表格到本地** @param file      本地文件对象* @param sheetData 导出数据*/public static void exportFile(File file, List<List<Object>> sheetData) {if (file == null) {System.out.println("文件创建失败");return;}if (sheetData == null) {sheetData = new ArrayList<>();}Map<String, List<List<Object>>> map = new HashMap<>();map.put(file.getName(), sheetData);export(null, file, file.getName(), map, null);}/*** 导出表格到本地** @param <T>      导出数据类似,和K类型保持一致* @param filePath 文件父路径(如:D:/doc/excel/)* @param fileName 文件名称(不带尾缀,如:学生表)* @param list     导出数据* @throws IOException IO异常*/public static <T> File exportFile(String filePath, String fileName, List<T> list) throws IOException {File file = getFile(filePath, fileName);List<List<Object>> sheetData = getSheetData(list);exportFile(file, sheetData);return file;}/*** 获取文件** @param filePath filePath 文件父路径(如:D:/doc/excel/)* @param fileName 文件名称(不带尾缀,如:用户表)* @return 本地File文件对象*/private static File getFile(String filePath, String fileName) throws IOException {String dirPath = getString(filePath);String fileFullPath;if (dirPath.isEmpty()) {fileFullPath = fileName;} else {// 判定文件夹是否存在,如果不存在,则级联创建File dirFile = new File(dirPath);if (!dirFile.exists()) {boolean mkdirs = dirFile.mkdirs();if (!mkdirs) {return null;}}// 获取文件夹全名if (dirPath.endsWith(String.valueOf(LEAN_LINE))) {fileFullPath = dirPath + fileName + XLSX;} else {fileFullPath = dirPath + LEAN_LINE + fileName + XLSX;}}System.out.println(fileFullPath);File file = new File(fileFullPath);if (!file.exists()) {boolean result = file.createNewFile();if (!result) {return null;}}return file;}private static <T> List<List<Object>> getSheetData(List<T> list) {// 获取表头字段List<ExcelClassField> excelClassFieldList = getExcelClassFieldList(list.get(0).getClass());List<String> headFieldList = new ArrayList<>();List<Object> headList = new ArrayList<>();Map<String, ExcelClassField> headFieldMap = new HashMap<>();for (ExcelClassField each : excelClassFieldList) {String fieldName = each.getFieldName();headFieldList.add(fieldName);headFieldMap.put(fieldName, each);headList.add(each.getName());}// 添加表头名称List<List<Object>> sheetDataList = new ArrayList<>();sheetDataList.add(headList);// 获取表数据for (T t : list) {Map<String, Object> fieldDataMap = getFieldDataMap(t);Set<String> fieldDataKeys = fieldDataMap.keySet();List<Object> rowList = new ArrayList<>();for (String headField : headFieldList) {if (!fieldDataKeys.contains(headField)) {continue;}Object data = fieldDataMap.get(headField);if (data == null) {rowList.add("");continue;}ExcelClassField cf = headFieldMap.get(headField);// 判断是否有映射关系LinkedHashMap<String, String> kvMap = cf.getKvMap();if (kvMap == null || kvMap.isEmpty()) {rowList.add(data);continue;}String val = kvMap.get(data.toString());if (isNumeric(val)) {rowList.add(Double.valueOf(val));} else {rowList.add(val);}}sheetDataList.add(rowList);}return sheetDataList;}private static <T> Map<String, Object> getFieldDataMap(T t) {Map<String, Object> map = new HashMap<>();Field[] fields = t.getClass().getDeclaredFields();try {for (Field field : fields) {String fieldName = field.getName();field.setAccessible(true);Object object = field.get(t);map.put(fieldName, object);}} catch (IllegalArgumentException | IllegalAccessException e) {e.printStackTrace();}return map;}public static void exportEmpty(HttpServletResponse response, String fileName) {List<List<Object>> sheetDataList = new ArrayList<>();List<Object> headList = new ArrayList<>();headList.add("导出无数据");sheetDataList.add(headList);export(response, fileName, sheetDataList);}public static void export(HttpServletResponse response, String fileName, List<List<Object>> sheetDataList) {export(response, fileName, fileName, sheetDataList, null);}public static void exportManySheet(HttpServletResponse response, String fileName, Map<String, List<List<Object>>> sheetMap) {export(response, null, fileName, sheetMap, null);}public static void export(HttpServletResponse response, String fileName, String sheetName,List<List<Object>> sheetDataList) {export(response, fileName, sheetName, sheetDataList, null);}public static void export(HttpServletResponse response, String fileName, String sheetName,List<List<Object>> sheetDataList, Map<Integer, List<String>> selectMap) {Map<String, List<List<Object>>> map = new HashMap<>();map.put(sheetName, sheetDataList);export(response, null, fileName, map, selectMap);}public static <T, K> void export(HttpServletResponse response, String fileName, List<T> list, Class<K> template) {// list 是否为空boolean lisIsEmpty = list == null || list.isEmpty();// 如果模板数据为空,且导入的数据为空,则导出空文件if (template == null && lisIsEmpty) {exportEmpty(response, fileName);return;}// 如果 list 数据,则导出模板数据if (lisIsEmpty) {exportTemplate(response, fileName, template);return;}// 导出数据List<List<Object>> sheetDataList = getSheetData(list);export(response, fileName, sheetDataList);}public static void export(HttpServletResponse response, String fileName, List<List<Object>> sheetDataList, Map<Integer, List<String>> selectMap) {export(response, fileName, fileName, sheetDataList, selectMap);}private static void export(HttpServletResponse response, File file, String fileName,Map<String, List<List<Object>>> sheetMap, Map<Integer, List<String>> selectMap) {// 整个 Excel 表格 book 对象SXSSFWorkbook book = new SXSSFWorkbook();// 每个 Sheet 页Set<Entry<String, List<List<Object>>>> entries = sheetMap.entrySet();for (Entry<String, List<List<Object>>> entry : entries) {List<List<Object>> sheetDataList = entry.getValue();Sheet sheet = book.createSheet(entry.getKey());Drawing<?> patriarch = sheet.createDrawingPatriarch();// 设置表头背景色(灰色)CellStyle headStyle = book.createCellStyle();headStyle.setFillForegroundColor(IndexedColors.GREY_80_PERCENT.index);headStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);headStyle.setAlignment(HorizontalAlignment.CENTER);headStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.index);// 设置表身背景色(默认色)CellStyle rowStyle = book.createCellStyle();rowStyle.setAlignment(HorizontalAlignment.CENTER);rowStyle.setVerticalAlignment(VerticalAlignment.CENTER);// 设置表格列宽度(默认为15个字节)sheet.setDefaultColumnWidth(15);// 创建合并算法数组int rowLength = sheetDataList.size();int columnLength = sheetDataList.get(0).size();int[][] mergeArray = new int[rowLength][columnLength];for (int i = 0; i < sheetDataList.size(); i++) {// 每个 Sheet 页中的行数据Row row = sheet.createRow(i);List<Object> rowList = sheetDataList.get(i);for (int j = 0; j < rowList.size(); j++) {// 每个行数据中的单元格数据Object o = rowList.get(j);int v = 0;if (o instanceof URL) {// 如果要导出图片的话, 链接需要传递 URL 对象setCellPicture(book, row, patriarch, i, j, (URL) o);} else {Cell cell = row.createCell(j);if (i == 0) {// 第一行为表头行,采用灰色底背景v = setCellValue(cell, o, headStyle);} else {// 其他行为数据行,默认白底色v = setCellValue(cell, o, rowStyle);}}mergeArray[i][j] = v;}}// 合并单元格mergeCells(sheet, mergeArray);// 设置下拉列表setSelect(sheet, selectMap);}// 写数据if (response != null) {// 前端导出try {write(response, book, fileName);} catch (IOException e) {e.printStackTrace();}} else {// 本地导出FileOutputStream fos;try {fos = new FileOutputStream(file);ByteArrayOutputStream ops = new ByteArrayOutputStream();book.write(ops);fos.write(ops.toByteArray());fos.close();} catch (Exception e) {e.printStackTrace();}}}/*** 合并当前Sheet页的单元格** @param sheet      当前 sheet 页* @param mergeArray 合并单元格算法*/private static void mergeCells(Sheet sheet, int[][] mergeArray) {// 横向合并for (int x = 0; x < mergeArray.length; x++) {int[] arr = mergeArray[x];boolean merge = false;int y1 = 0;int y2 = 0;for (int y = 0; y < arr.length; y++) {int value = arr[y];if (value == CELL_COLUMN_MERGE) {if (!merge) {y1 = y;}y2 = y;merge = true;} else {merge = false;if (y1 > 0) {sheet.addMergedRegion(new CellRangeAddress(x, x, (y1 - 1), y2));}y1 = 0;y2 = 0;}}if (y1 > 0) {sheet.addMergedRegion(new CellRangeAddress(x, x, (y1 - 1), y2));}}// 纵向合并int xLen = mergeArray.length;int yLen = mergeArray[0].length;for (int y = 0; y < yLen; y++) {boolean merge = false;int x1 = 0;int x2 = 0;for (int x = 0; x < xLen; x++) {int value = mergeArray[x][y];if (value == CELL_ROW_MERGE) {if (!merge) {x1 = x;}x2 = x;merge = true;} else {merge = false;if (x1 > 0) {sheet.addMergedRegion(new CellRangeAddress((x1 - 1), x2, y, y));}x1 = 0;x2 = 0;}}if (x1 > 0) {sheet.addMergedRegion(new CellRangeAddress((x1 - 1), x2, y, y));}}}private static void write(HttpServletResponse response, SXSSFWorkbook book, String fileName) throws IOException {response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");response.setCharacterEncoding("utf-8");String name = new String(fileName.getBytes("GBK"), "ISO8859_1") + XLSX;response.addHeader("Content-Disposition", "attachment;filename=" + name);ServletOutputStream out = response.getOutputStream();book.write(out);out.flush();out.close();}private static int setCellValue(Cell cell, Object o, CellStyle style) {// 设置样式cell.setCellStyle(style);// 数据为空时if (o == null) {cell.setCellType(CellType.STRING);cell.setCellValue("");return CELL_OTHER;}// 是否为字符串if (o instanceof String) {String s = o.toString();// 当数字类型长度超过8位时,改为字符串类型显示(Excel数字超过一定长度会显示为科学计数法)if (isNumeric(s) && s.length() < 8) {cell.setCellType(CellType.NUMERIC);cell.setCellValue(Double.parseDouble(s));return CELL_OTHER;} else {cell.setCellType(CellType.STRING);cell.setCellValue(s);}if (s.equals(ROW_MERGE)) {return CELL_ROW_MERGE;} else if (s.equals(COLUMN_MERGE)) {return CELL_COLUMN_MERGE;} else {return CELL_OTHER;}}// 是否为字符串if (o instanceof Integer || o instanceof Long || o instanceof Double || o instanceof Float) {cell.setCellType(CellType.NUMERIC);cell.setCellValue(Double.parseDouble(o.toString()));return CELL_OTHER;}// 是否为Booleanif (o instanceof Boolean) {cell.setCellType(CellType.BOOLEAN);cell.setCellValue((Boolean) o);return CELL_OTHER;}// 如果是BigDecimal,则默认3位小数if (o instanceof BigDecimal) {cell.setCellType(CellType.NUMERIC);cell.setCellValue(((BigDecimal) o).setScale(3, RoundingMode.HALF_UP).doubleValue());return CELL_OTHER;}// 如果是Date数据,则显示格式化数据if (o instanceof Date) {cell.setCellType(CellType.STRING);cell.setCellValue(formatDate((Date) o));return CELL_OTHER;}// 如果是其他,则默认字符串类型cell.setCellType(CellType.STRING);cell.setCellValue(o.toString());return CELL_OTHER;}private static void setCellPicture(SXSSFWorkbook wb, Row sr, Drawing<?> patriarch, int x, int y, URL url) {// 设置图片宽高sr.setHeight((short) (IMG_WIDTH * IMG_HEIGHT));// (jdk1.7版本try中定义流可自动关闭)try (InputStream is = url.openStream(); ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {byte[] buff = new byte[BYTES_DEFAULT_LENGTH];int rc;while ((rc = is.read(buff, 0, BYTES_DEFAULT_LENGTH)) > 0) {outputStream.write(buff, 0, rc);}// 设置图片位置XSSFClientAnchor anchor = new XSSFClientAnchor(0, 0, 0, 0, y, x, y + 1, x + 1);// 设置这个,图片会自动填满单元格的长宽anchor.setAnchorType(AnchorType.MOVE_AND_RESIZE);patriarch.createPicture(anchor, wb.addPicture(outputStream.toByteArray(), HSSFWorkbook.PICTURE_TYPE_JPEG));} catch (Exception e) {e.printStackTrace();}}private static String formatDate(Date date) {if (date == null) {return "";}SimpleDateFormat format = new SimpleDateFormat(DATE_FORMAT);return format.format(date);}private static void setSelect(Sheet sheet, Map<Integer, List<String>> selectMap) {if (selectMap == null || selectMap.isEmpty()) {return;}Set<Entry<Integer, List<String>>> entrySet = selectMap.entrySet();for (Entry<Integer, List<String>> entry : entrySet) {int y = entry.getKey();List<String> list = entry.getValue();if (list == null || list.isEmpty()) {continue;}String[] arr = new String[list.size()];for (int i = 0; i < list.size(); i++) {arr[i] = list.get(i);}DataValidationHelper helper = sheet.getDataValidationHelper();CellRangeAddressList addressList = new CellRangeAddressList(1, 65000, y, y);DataValidationConstraint dvc = helper.createExplicitListConstraint(arr);DataValidation dv = helper.createValidation(dvc, addressList);if (dv instanceof HSSFDataValidation) {dv.setSuppressDropDownArrow(false);} else {dv.setSuppressDropDownArrow(true);dv.setShowErrorBox(true);}sheet.addValidationData(dv);}}private static boolean isNumeric(String str) {if (Objects.nonNull(str) && "0.0".equals(str)) {return true;}for (int i = str.length(); --i >= 0; ) {if (!Character.isDigit(str.charAt(i))) {return false;}}return true;}private static String getString(String s) {if (s == null) {return "";}if (s.isEmpty()) {return s;}return s.trim();}}

ExcelImport

package com.zyq.util.excel;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;/*** @author sunnyzyq* @date 2021/12/17*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExcelImport {/** 字段名称 */String value();/** 导出映射,格式如:0-未知;1-男;2-女 */String kv() default "";/** 是否为必填字段(默认为非必填) */boolean required() default false;/** 最大长度(默认255) */int maxLength() default 255;/** 导入唯一性验证(多个字段则取联合验证) */boolean unique() default false;}

ExcelExport

package com.zyq.util.excel;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;/*** @author sunnyzyq* @date 2021/12/17*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExcelExport {/** 字段名称 */String value();/** 导出排序先后: 数字越小越靠前(默认按Java类字段顺序导出) */int sort() default 0;/** 导出映射,格式如:0-未知;1-男;2-女 */String kv() default "";/** 导出模板示例值(有值的话,直接取该值,不做映射) */String example() default "";}

ExcelClassField

package com.zyq.util.excel;import java.util.LinkedHashMap;/*** @author sunnyzyq* @date 2021/12/17*/
public class ExcelClassField {/** 字段名称 */private String fieldName;/** 表头名称 */private String name;/** 映射关系 */private LinkedHashMap<String, String> kvMap;/** 示例值 */private Object example;/** 排序 */private int sort;/** 是否为注解字段:0-否,1-是 */private int hasAnnotation;public String getFieldName() {return fieldName;}public void setFieldName(String fieldName) {this.fieldName = fieldName;}public String getName() {return name;}public void setName(String name) {this.name = name;}public LinkedHashMap<String, String> getKvMap() {return kvMap;}public void setKvMap(LinkedHashMap<String, String> kvMap) {this.kvMap = kvMap;}public Object getExample() {return example;}public void setExample(Object example) {this.example = example;}public int getSort() {return sort;}public void setSort(int sort) {this.sort = sort;}public int getHasAnnotation() {return hasAnnotation;}public void setHasAnnotation(int hasAnnotation) {this.hasAnnotation = hasAnnotation;}}

3. 更新优化

3.1 优化身份证导出显示为科学计数法的问题(2022-07-04)

优化前:身份证号码显示为科学计数法。

优化后:身份证号码正常显示。

4. 疑问解答

4.1 关于身份证导入显示科学计数法的问题(2022-07-04)

首先,当我们把身份证输入到表格中的时候,表格会默认为数字类型,由于数字太大,这时候Excel 会自动显示为科学计数法。

那么这个时候,我们进行导入的话,确实会显示为科学计数法,如下图所示:

那么,如果要解决这个问题,需要从根源解决掉,即让 Excel 表格不能显示为科学计数法。

这个时候,我们需要将该列设置为文本格式:

然后设置为文本格式。

当设置完成后,你再重新输入身份证后,就会原封不动的显示了。

这个时候,你再导入就会完全显示了。

Java实现Excel导入和导出,看这一篇就够了(珍藏版)相关推荐

  1. 计算机二级excel高级筛选,Excel高级筛选,看这一篇就够了!

    原标题:Excel高级筛选,看这一篇就够了! Excel高级筛选,看这一篇就够了 Excel中的自动筛选很多人都会用,但经常遇到多条件或复杂条件筛选时卡壳的窘境,其实强大的Excel早就为你准备了N多 ...

  2. Java中的多线程你只要看这一篇就够了

    如果对什么是线程.什么是进程仍存有疑惑,请先Google之,因为这两个概念不在本文的范围之内. 用多线程只有一个目的,那就是更好的利用cpu的资源,因为所有的多线程代码都可以用单线程来实现.说这个话其 ...

  3. Java 中的多线程你只要看这一篇就够了

    作者丨纳达丶无忌 https://www.jianshu.com/p/40d4c7aebd66 引 如果对什么是线程.什么是进程仍存有疑惑,请先Google之,因为这两个概念不在本文的范围之内. 用多 ...

  4. Java高频重点面试题,看这一篇就够了。

    查缺补漏 一.基础篇 1. ==和equals区别? 2. spring 有哪些主要模块? 3. Spring中bean的生命周期 3.1 Spring Bean的生命周期指的是从一个普通的Java类 ...

  5. .net java 泛型_Java高级特性泛型看这一篇就够了

    作者:qwer1030274531 出自:ITPUB博客 1.为什么我们需要泛型? 通过两段代码就可以知道为什么需要泛型 /*** * 没有泛型的时候实现加法 */public class NonGe ...

  6. Unity 调用Java | 调用aar包 | 调用jar包 | Java内部类,附带实例,看我一篇就够啦~

  7. C# Excel导入、导出

    C# Excel导入.导出 本篇主要介绍C#的Excel导入.导出. 目录 1. 介绍:描述第三方类库NPOI以及Excel结构 2. Excel导入:介绍C#如何调用NPOI进行Excel导入,包含 ...

  8. java实现excel导入导出,对象图片读取,上传七牛云

    java实现excel导入导出以及解决方案 因为公司业务需求,要完成针对表格的导入导出,excel这里使用MultipartFile类接收 ,下面是部分关键代码,希望有所帮助 //获取excel文件的 ...

  9. Java Excel导入和导出(支持xls导入,xlsx导入,图片导出,百万数据量导出)

    免费源码下载(提取码:qdhy) 工程结构目录 所需JAR包 <dependencies><!-- JUNIT 测试 --><dependency><grou ...

  10. Java POI Excel导入导出

    Java POI Excel导入导出 1.maven引入依赖 2.导入Excel 3.导出Excel 1.maven引入依赖 <!-- POI Excel 操作 --> <depen ...

最新文章

  1. Apache 2.2 虚拟主机配置(本人推荐的)
  2. 【夸QT十一】外来物品:通用脚本帮助Web运行基础Linux命令
  3. VUE初长成【部分小记】
  4. 【Envi风暴】Envi 5.3 SP1经典安装手把手图文教程(含补丁文件)
  5. (网页)swiper.js轮播图插件
  6. 计算机联锁软件毕设论文,铁路信号计算机联锁系统()毕设论文.doc
  7. s5pv210 linux内核移植,S5PV210(TQ210)学习笔记——内核移植与文件系统构建之二
  8. mysql relay log 配置_mysql relay log参数汇总
  9. 线程安全问题的本质详解: 原子性、有序性、可见性
  10. SQL 模糊表名查询
  11. Effective C++ -----条款42:了解typename的双重意义
  12. .Net WebApi接口之Swagger集成详解
  13. mysql 逻辑备份工具_MySQL逻辑备份工具-mydumper
  14. 全网最全移动端App性能测试方法,值得收藏!
  15. NoSQL:文档数据库
  16. 4本图神经网络中文书籍的比较
  17. 【代码超详解】LightOJ 1197 Help Hanzo(区间质数筛法)
  18. A79T三极管,A79T芯片规格书
  19. 室内地图导航系统基础功能与衍生服务详解
  20. 拼多多势不可挡的发展|一度智信

热门文章

  1. SQL注入攻击与防御学习笔记一
  2. c语言plc编程软件下载,罗克韦尔plc编程软件下载-2017罗克韦尔plc编程软件V21.00 官方最新版 - 极光下载站...
  3. 运维之linux系统故障排查思路
  4. java项目计算器关键思路_JAVA项目实战(计算器)
  5. Python和Java哪个更好找工作?
  6. AXURE-手把手教你做汉化
  7. ikm java_ikm(IKM在线)
  8. 直播开发中关于三级分销功能源码的实现介绍
  9. 北斗导航 | RAIM算法之奇偶矢量法(原理讲解,附代码链接:可用性判定)
  10. 【SaaS】企业微信裂变系统引流变现系统产品介绍