EasyExcel: 结合springboot实现表格导出入(单/多sheet), 全字段校验,批次等操作(全)
全文目录,一步到位
- 1.前言简介
- 1.1 链接传送门
- 1.1.1 easyExcel传送门
- 2. Excel表格导入过程
- 2.1 easyExcel的使用准备工作
- 2.1.1 导入maven依赖
- 2.1.2 建立一个util包
- 2.1.3 ExcelUtils统一功能封装(单/多sheet导入)
- 2.1.4 ExcelDataListener数据监听器
- 2.1.5 ResponseHelper响应值处理
- 2.1.6 MyConverter类-自定义转换器
- 2.1.7 ExcelDataService
- 2.1.8 ExcelReqDTO 统一请求dto
- 2.1.9 上传文件校验
- 2.1.10 最后写个readme.md(说明使用方式)
- 2.2 easyExcel工具包(全)使用方式
- 2.2.1 UserExcelDTO 生成用户excel数据
- 2.2.2 ExcelDataServiceImpl实现类(工程一)
- 2.2.3 upload.html测试页面
- 3.业务实战方式与效果(`可跳过2.2`)核心
- 3.1 业务工具类
- 3.1.1 ThreadLocalUtils工具类(批次号)
- 3.1.2 自定义字段校验(注解)
- -> 3.1.2_1 创建校验注解`@DataCheck`
- -> 3.1.2_2 注解实现类ValidatorUtils(校验逻辑)
- 3.2 工程内业务使用
- 3.2.0 创建上传或下载对象dto
- 3.2.1 创建controller
- 3.2.2 接口SystemExcelService
- 3.2.3 实现类SystemExcelServiceImpl(需根业务自行调整)
- 3.2.4 寻找ExcelDataService的实现类
- 3.3 程序测试执行结果及报错解决
- 3.3.1 执行结果
- 3.3.2 报错解决
- 3.3.2_1 CROS跨域问题
- 3.3.2_2 excel表格导出是空
- 3.3.2_3 导入dto中有list报错
- 3.3.2_4 导出模板/sheet的名字不正确
- 3.3.2_5 待续未完...
- 4. 文章的总结与预告
- 4.1 本文总结
- 4.2 下文预告
1.前言简介
ps: 如您有更好的方案或发现错误,请不吝赐教,感激不尽啦~~~
使用了easyExcel实现导入操作, 全手动封装, 灵活使用, 为了满足部分业务需求, 也做了
升级
全字段
进行校验, 使用注解与正则
表达式, 校验到每一行
参数- 报错
信息明确
, 精确到每一行, 某个字段不正确的报错- 多个sheet导入的excel, 提示出
sheet名下的第几行
报错- 增加
xid同批次报错
回滚, 有点类似分布式事务, 也就是一行报错,全部批次数据清除- 增加拓展性, 制作
监听器,样式封装
等, 利用接口特性, 方便多工程使用拓展- 在特殊类型(如list等类型)导入时, 出现了报错, 进行了兼容操作
- 增加了数据库插入
批次新增
, 防止推数据库的数据量过大
, 业务才略微麻烦
1.1 链接传送门
1.1.1 easyExcel传送门
⇒ EasyExcel文档链接
⇒ EasyExcel-Plus尽情期待~~~
2. Excel表格导入过程
实现功能请看
1 前言简介
, 里面有详细说明
2.1 easyExcel的使用准备工作
2.1.1 导入maven依赖
<alibaba.easyexcel.version>3.3.4</alibaba.easyexcel.version>
<!-- easyExcel --><dependency><groupId>com.alibaba</groupId><artifactId>easyexcel</artifactId><version>${alibaba.easyexcel.version}</version></dependency>
2.1.2 建立一个util包
里面专门放置全部的excel操作, 如图所示
realDto
里面就是具体导入业务dtotestGroup
是自行测试代码- 其他类均为
核心逻辑
-readme.md
是使用说明, 防止后面人不知道如何使用
下面从2.1.3开始
2.1.3 ExcelUtils统一功能封装(单/多sheet导入)
跳转链接: 解释 @Accessors(chain = true) 与 easyExcel不兼容
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelReader;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.read.listener.ReadListener;
import com.alibaba.excel.read.metadata.ReadSheet;
import com.alibaba.excel.write.builder.ExcelWriterSheetBuilder;
import com.alibaba.excel.write.handler.WriteHandler;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.google.common.collect.Lists;
import lombok.extern.slf4j.Slf4j;import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.rmi.ServerException;
import java.util.List;/*** Excel相关操作(简易)* 文章一: 解释 @Accessors(chain = true) 与 easyExcel不兼容* -> https://blog.csdn.net/qq_36268103/article/details/134954322** @author pzy* @version 1.1.0* @description ok*/
@Slf4j
public class ExcelUtils {/*** 方法1.1: 读取excel(单sheet)** @param inputStream 输入流* @param dataClass 任意类型* @param listener 监听* @param sheetNo sheet编号* @param <T> 传入类型*/public static <T> void readExcel(InputStream inputStream, Class<T> dataClass, ReadListener<T> listener, int sheetNo) {try (ExcelReader excelReader = EasyExcel.read(inputStream, dataClass, listener).build()) {// 构建一个sheet 这里可以指定名字或者noReadSheet readSheet = EasyExcel.readSheet(sheetNo).build();// 读取一个sheetexcelReader.read(readSheet);}}/*** 方法2.1: 读取excel(多sheet)** @param inputStream 输入流* @param dataClass 任意类型* @param listener 监听* @param sheetNoList sheet编号* @param <T> 传入类型*/public static <T> void readExcel(InputStream inputStream, Class<T> dataClass, ReadListener<T> listener, List<Integer> sheetNoList) {try (ExcelReader excelReader = EasyExcel.read(inputStream, dataClass, listener).build()) {List<ReadSheet> readSheetList = Lists.newArrayList();sheetNoList.forEach(sheetNo -> {// 构建一个sheet 这里可以指定名字或者noReadSheet readSheet = EasyExcel.readSheet(sheetNo).build();readSheetList.add(readSheet);});// 读取一个sheetexcelReader.read(readSheetList);}}/*** 单sheet excel下载** @param httpServletResponse 响应对象* @param fileName excel文件名字* @param dataClass class类型(转换)* @param sheetName sheet位置1的名字* @param dataList 传入的数据* @param writeHandlers 写处理器们 可变参数 (样式)* @param <T> 泛型*/public static <T> void easyDownload(HttpServletResponse httpServletResponse,String fileName,Class<T> dataClass,String sheetName,List<T> dataList,WriteHandler... writeHandlers) throws IOException {//对响应值进行处理getExcelServletResponse(httpServletResponse, fileName);ExcelWriterSheetBuilder builder =EasyExcel.write(httpServletResponse.getOutputStream(), dataClass).sheet(sheetName);
//
// builder.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
// .registerWriteHandler(ExcelStyleTool.getStyleStrategy());/*样式处理器*/if (writeHandlers.length > 0) {for (WriteHandler writeHandler : writeHandlers) {builder.registerWriteHandler(writeHandler);}}builder.doWrite(dataList);}/*** 复杂 excel下载* 1. 多个sheet* 2. 多个处理器** @param httpServletResponse 响应对象* @param fileName excel文件名字* @param dataClass class类型(转换)* @param sheetNameList 多sheet的名字数据* @param sheetDataList 多sheet的实际数据* @param writeHandlers 写处理器们 可变参数 (样式)* @param <T> 泛型*/public static <T> void complexDownload(HttpServletResponse httpServletResponse,String fileName,Class<T> dataClass,List<String> sheetNameList,List<List<T>> sheetDataList,WriteHandler... writeHandlers) throws IOException {if (sheetNameList.size() != sheetDataList.size()) {throw new ServerException("抱歉,名字与列表长度不符~");}//对响应值进行处理getExcelServletResponse(httpServletResponse, fileName);try (ExcelWriter excelWriter = EasyExcel.write(httpServletResponse.getOutputStream()).build()) {// 去调用写入, 这里最终会写到多个sheet里面for (int i = 0; i < sheetNameList.size(); i++) {ExcelWriterSheetBuilder builder = EasyExcel.writerSheet(i, sheetNameList.get(i)).head(dataClass);if (writeHandlers.length > 0) {for (WriteHandler writeHandler : writeHandlers) {builder.registerWriteHandler(writeHandler);}}WriteSheet writeSheet = builder.build();excelWriter.write(sheetDataList.get(i), writeSheet);}}}/*** 获取excel的响应对象** @param httpServletResponse response* @param fileName 文件名* @throws UnsupportedEncodingException 不支持编码异常*/private static void getExcelServletResponse(HttpServletResponse httpServletResponse, String fileName) throws UnsupportedEncodingException {// 设置URLEncoder.encode可以防止中文乱码,和easyexcel没有关系fileName = URLEncoder.encode(fileName, "UTF-8").replaceAll("\\+", "%20");httpServletResponse.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");httpServletResponse.setCharacterEncoding("utf-8");httpServletResponse.addHeader("Access-Control-Expose-Headers", "Content-Disposition");httpServletResponse.setHeader("Content-Disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");}
2.1.4 ExcelDataListener数据监听器
读取excel表格数据 一条一条读取出来
ps: ResultResponse就是返回值封装类 随便都行200或500
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.read.listener.ReadListener;
import com.alibaba.excel.util.ListUtils;import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.google.common.collect.Lists;
import lombok.extern.slf4j.Slf4j;import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;/*** 官方提供转换listener* ps: 有个很重要的点 ExcelDataListener 不能被spring管理,要每次读取excel都要new,然后里面用到spring可以构造方法传进去** @author pzy* @version 0.1.0* @description ok*/
//@Component
@Slf4j
public class ExcelDataListener<T> implements ReadListener<T> {/*** 每隔5条存储数据库,实际使用中可以300条,然后清理list ,方便内存回收*/private static final int BATCH_COUNT = 300;private final ConcurrentHashMap<String, AtomicInteger> map = new ConcurrentHashMap<>();/*** 缓存的数据*/
// private List<T> cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);private final List<T> cachedDataList = Lists.newCopyOnWriteArrayList();/*** 假设这个是一个DAO,当然有业务逻辑这个也可以是一个service。当然如果不用存储这个对象没用。*/private final ExcelDataService excelDataService;/*** 自行定义的功能类型 1配件(库存) 2供应商 3客户(假)资料*/private final Integer functionType;/*** 如果使用了spring,请使用这个构造方法。每次创建Listener的时候需要把spring管理的类传进来*/public ExcelDataListener(ExcelDataService excelDataService1, Integer functionType) {this.excelDataService = excelDataService1;this.functionType = functionType;}/*** 这个每一条数据解析都会来调用** @param data one row value. Is is same as {@link AnalysisContext#readRowHolder()}*/@Overridepublic void invoke(T data, AnalysisContext context) {// String threadName = Thread.currentThread().getName();
// System.out.println(threadName);log.info("解析到一条数据:{}", JSON.toJSONString(data));String sheetName = context.readSheetHolder().getSheetName();//ps: 慢换LongAdder
// if (!map.containsKey(sheetName)) {
// map.put(sheetName, new AtomicInteger(0));
// } else {
// map.put(sheetName, new AtomicInteger(map.get(sheetName).incrementAndGet()));
// }int sheetDataCounts = map.computeIfAbsent(sheetName, k -> new AtomicInteger(0)).incrementAndGet();log.info("当前sheet的数据是: {}, 数量是第: {}个", sheetName, sheetDataCounts);if (data != null) {JSONObject jsonObject = JSON.parseObject(JSON.toJSONString(data));jsonObject.put("sheetName", sheetName);jsonObject.put("sheetDataNo", sheetDataCounts);//放入sheet数据编号(如果仅一个sheetcachedDataList.add((T) jsonObject);//类型明确(不增加通配符边界了 增加使用难度)}// 达到BATCH_COUNT了,需要去存储一次数据库,防止数据几万条数据在内存,容易OOMif (cachedDataList.size() >= BATCH_COUNT) {saveData();// 存储完成清理 list
// cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);cachedDataList.clear();//这块需要测试看看效果}}/*** 所有数据解析完成了 都会来调用*/@Overridepublic void doAfterAllAnalysed(AnalysisContext context) {// 这里也要保存数据,确保最后遗留的数据也存储到数据库log.info("{}条数据,开始存储数据库!", cachedDataList.size());saveData();cachedDataList.clear();log.info("所有数据解析完成!");}/*** 加上存储数据库*/private void saveData() {log.info("{}条数据,开始存储数据库!", cachedDataList.size());
// excelDataService.saveUser((T) new SystemUser());ResultResponse response = excelDataService.saveExcelData(functionType, cachedDataList);if (ResponseHelper.judgeResp(response)) {log.info("存储数据库成功!");}}}
2.1.5 ResponseHelper响应值处理
ResultResponse返回值
封装类 任意即可
import com.alibaba.fastjson.TypeReference;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;import javax.servlet.http.HttpServletRequest;
import java.util.Objects;/*** 响应 工具类** @author pzy* @version 0.1.0* @description ok*/
public class ResponseHelper<T> {/*** 响应成功失败校验器* 疑似存在bug(未进行测试)*/@Deprecatedpublic T retBool(ResultResponse response) {/*1. 如果接口返回值返回的不是200 抛出异常*/if (response.getCode() != 200) {throw new ServiceException(response.getCode(), response.getMsg());}return response.getData(new TypeReference<T>() {});}/*** 请求响应值校验器(ResultResponse对象)*/public static void retBoolResp(ResultResponse response) {if (response == null) {throw new NullPointerException("服务响应异常!");}/*1. 如果接口返回值返回的不是200 抛出异常*/if (!Objects.equals(response.getCode(), 200)) {throw new ServiceException(response.getCode(), response.getMsg());}}/*** 判定响应返回值* <p>* true 表示200 服务通畅* false 表示500 服务不通畅(*/public static boolean judgeResp(ResultResponse response) {// 1. 如果接口返回值返回的不是200 返回falsereturn response != null && Objects.equals(response.getCode(), 200);}/*** 通过上下文对象获取请求头的token值* RequestHelper.getHeaderToken()*/@Deprecatedpublic static String getHeaderToken() {//请求上下文对象获取 线程RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();assert requestAttributes != null;HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);assert request != null;return request.getHeader("token");}}
2.1.6 MyConverter类-自定义转换器
@ExcelProperty(converter = MyConverter.class) 使用自定义转换器 针对list等类型进行操作
import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.converters.ReadConverterContext;
import com.alibaba.excel.converters.WriteConverterContext;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.data.WriteCellData;import java.util.Collections;
import java.util.List;
import java.util.StringJoiner;/*** list类型使用 自定义转换器(补充功能 beta版)* @author pzy* @version 0.1.0* @description ok*/
public class MyConverter implements Converter<List> {@Overridepublic Class<?> supportJavaTypeKey() {return List.class;}@Overridepublic CellDataTypeEnum supportExcelTypeKey() {return CellDataTypeEnum.STRING;}/*** 读(导入)数据时调用*/@Overridepublic List convertToJavaData(ReadConverterContext<?> context) {//当字段使用@ExcelProperty(converter = MyConverter.class)注解时会调用//context.getReadCellData().getStringValue()会获取excel表格中该字段对应的String数据//这里可以对数据进行额外的加工处理String stringValue = context.getReadCellData().getStringValue();//将数据转换为List类型然后返回给实体类对象DTOreturn Collections.singletonList(stringValue);}/*** 写(导出)数据时调用*/@Overridepublic WriteCellData<?> convertToExcelData(WriteConverterContext<List> context) {//当字段使用@ExcelProperty(converter = MyConverter.class)注解时会调用//context.getValue()会获取对应字段的List类型数据//这里是将List<String>转换为String类型数据,根据自己的数据进行处理StringJoiner joiner = new StringJoiner(",");for (Object data : context.getValue()) {joiner.add((CharSequence) data);}//然后将转换后的String类型数据写入到Excel表格对应字段当中return new WriteCellData<>(joiner.toString());}
}
2.1.7 ExcelDataService
数据处理行为接口(
多工程拓展
)
import java.util.List;/*** 数据处理service** @author pzy* @version 0.1.0* @description ok*/
@FunctionalInterface
public interface ExcelDataService {/*** 保存导入的数据* 分批进入 防止数据过大 - 栈溢出** @param t 保存的数据类型*/<T> ResultResponse saveExcelData(Integer functionType, List<T> t);
}
2.1.8 ExcelReqDTO 统一请求dto
业务需要, 生成的
文件名
sheet
的名称 功能类型等信息
其中Lists.newArrayList() 没有的直接换成new ArrayList()
即可 效果相同
import com.google.common.collect.Lists;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;import java.util.List;/*** excel统一请求dto* <p>* 传入需要的参数, 生成对应的excel表格** @author pzy* @version 0.1.0* @description ok*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public class ExcelReqDTO {/*** 功能类型 例: 1用户 2其他业务*/private Integer functionType;/*** excel类型 1单sheet 2多sheet*/private Integer excelType;/*** 文件名称*/private String fileName;/*** sheet名称*/private String sheetName;/*** sheet名称组*/private List<String> sheetNames = Lists.newArrayList();
}
2.1.9 上传文件校验
文件大小校验可在配置文件内添加, 效果更好
import lombok.extern.slf4j.Slf4j;import org.springframework.web.multipart.MultipartFile;import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Arrays;
import java.util.Locale;/*** 文件上传校验的公共方法* 严格校验** @author pzy* @version 1.0.0*/
@Slf4j
public class UploadCheckUtils {//20MBprivate static final Integer maxUpLoadSize = 20;/*** 只支持文件格式*/public static final String[] YES_FILE_SUPPORT = {".xlsx", ".xls", ".doc", ".docx", ".txt", ".csv"};/*** 全部文件(普通文件,图片, 视频,音频)后缀 支持的类型*/private static final String[] FILE_SUFFIX_SUPPORT = {".xlsx", ".xls", ".doc", ".docx", ".txt", ".csv",".jpg", ".jpeg", ".png", ".mp4", ".avi", ".mp3"};/*** 文件名字 需要排除的字符* 废弃: "(", ")","",".", "——", "_","-"*/private static final String[] FILE_NAME_EXCLUDE = {"`", "!", "@", "#", "$", "%", "^", "&", "*", "=", "+","~", "·", "!", "¥", "……", "(", ")","?", ",", "<", ">", ":", ";", "[", "]", "{", "}", "/", "\\", "|","?", ",", "。", "《", "》", ":", ";", "【", "】", "、"};/*** 多文件上传* 校验+大图片压缩*/public MultipartFile[] uploadVerify(MultipartFile[] multipartFile) {/*校验1: 没有文件时,报错提示*/if (multipartFile == null || multipartFile.length <= 0) {throw new ServiceException(500, "上传文件不能为空");}/*总文件大于: ?Mb时, 拦截*/long sumSize = 0;for (MultipartFile file : multipartFile) {sumSize += file.getSize();}// 总文件超过100mb 直接拦截 beta功能 不正式使用if (sumSize > (100 * 1024 * 1024L)) {log.warn("(上传总空间)大于100MB, 文件上传过大!");
// throw new ThirdServiceException(ResponseEnum.T160007, "(上传总空间)100");}/*校验2: 上传文件的长度小于等于1 就一个直接校验*/if (multipartFile.length <= 1) {MultipartFile[] files = new MultipartFile[1];files[0] = uploadVerify(multipartFile[0]);return files;}/*校验3: 多个文件直接校验 需要更换新的file */for (int i = 0; i < multipartFile.length; i++) {multipartFile[i] = uploadVerify(multipartFile[i]);}return multipartFile;}/*** 上传文件校验大小、名字、后缀** @param multipartFile multipartFile*/public static MultipartFile uploadVerify(MultipartFile multipartFile) {// 校验文件是否为空if (multipartFile == null) {throw new ServiceException(500, "上传文件不能为空呦~");}/*大小校验*/log.info("上传文件的大小的是: {} MB", new BigDecimal(multipartFile.getSize()).divide(BigDecimal.valueOf(1024 * 1024), CommonConstants.FINANCE_SCALE_LENGTH, RoundingMode.HALF_UP));log.info("上传限制的文件大小是: {} MB", maxUpLoadSize);if (multipartFile.getSize() > (maxUpLoadSize * 1024 * 1024L)) {throw new ServiceException(500, String.format("上传文件不得大于 %s MB", maxUpLoadSize));}// 校验文件名字String originalFilename = multipartFile.getOriginalFilename();if (originalFilename == null) {throw new ServiceException(500, "上传文件名字不能为空呦~");}for (String realKey : FILE_NAME_EXCLUDE) {if (originalFilename.contains(realKey)) {throw new ServiceException(500, String.format("文件名字不允许出现 '%s' 关键字呦~", realKey));}}// 校验文件后缀if (!originalFilename.contains(".")) {throw new ServiceException(500, "文件不能没有后缀呦~");}String suffix = originalFilename.substring(originalFilename.lastIndexOf('.'));/*校验: 文件格式是否符合要求*/if (!Arrays.asList(FILE_SUFFIX_SUPPORT).contains(suffix.toLowerCase(Locale.ROOT))) {//throw new RuntimeException("文件格式' " + realFormat + " '不支持,请更换后重试!");throw new ServiceException(500, "文件格式不支持呦~");}return multipartFile;}
}
2.1.10 最后写个readme.md(说明使用方式)
这里写不写都行,
如有错误,请指出
,谢谢啦~
# excel工具类使用说明## 1.本功能支持1. excel导入
2. excel导出
3. 样式调整
4. 类型转换器## 2. 使用技术介绍- 使用alibaba的easyExcel 3.3.4版本
- 官网地址: [=> easyExcel新手必读 ](https://easyexcel.opensource.alibaba.com/docs/current)## 3. 功能说明1. ExcelUtils 统一工具类 封装了单/多sheet的导入与导出 任意类型传入 只需`.class`即可
2. ExcelStyleTool excel表格导出风格自定义
3. MyConverter: 对于list类型转换存在问题, 手写新的转换器(beta版)
4. ExcelDataListener 数据监听器, 在这里处理接收的数据
5. ExcelDataService 数据处理服务接口(封装统一的功能要求, 同时满足拓展性)
6. testGroup中 全部均为演示demo(请在需要的工程中使用)## 4. 功能的演示1. upload.html 前端简易测试功能页面(测试功能)## 5. 版本说明1. beta版(1.0.1), 测试中
2. 可能有更好的方法解决本次业务需求
3. 导出的样式仅仅是简易能用, 跟美观没啥关系## 6. 特别注意1. 生成的excel的实体类均需要新写(或者看6-2)
2. @Accessors不可使用: 源码位置-> (ModelBuildEventListener的buildUserModel)中的BeanMap.create(resultModel).putAll(map);> [不能使用@Accessors(chain = true) 注解原因: ](https://blog.csdn.net/zmx729618/article/details/78363191)
>## 7. 本文作者
> @author: pzy
2.2 easyExcel工具包(全)使用方式
testGroup组演示
2.2.1 UserExcelDTO 生成用户excel数据
跟随业务随意, 用啥字段就加啥,
@ExcelIgnore
//表示忽略此字段
import com.alibaba.excel.annotation.ExcelIgnore;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import com.alibaba.excel.annotation.write.style.ContentRowHeight;
import com.alibaba.excel.annotation.write.style.HeadRowHeight;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;/*** excel表格示例demo* ps: 不能用accessors** @author pzy* @version 0.1.0* @description ok*/
@ContentRowHeight(20)
@HeadRowHeight(30)
@ColumnWidth(25)
@NoArgsConstructor
@AllArgsConstructor
//@Accessors(chain = true)
@Data
public class UserExcelDTO {/*** 用户ID*/
// @ExcelIgnore //忽略@ColumnWidth(20)@ExcelProperty(value = "用户编号")private Long userId;@ColumnWidth(50)@ExcelProperty(value = "真实姓名")private String realName;@ColumnWidth(50)@ExcelProperty(value = "手机号")private String phone;/*** 用户邮箱*/@ColumnWidth(50)//@ExcelProperty(value = "邮箱",converter = MyConverter.class)@ExcelProperty(value = "邮箱")private String email;}
2.2.2 ExcelDataServiceImpl实现类(工程一)
模拟一下数据库行为操作, 后面有实际操作呦~
import java.util.List;/*** 实现类 demo实现方式 (此处不可注入bean) 示例文档** @author pzy* @version 0.1.0* @description ok*/
//@Slf4j
//@RequiredArgsConstructor
//@Service
public class ExcelDataServiceImpl implements ExcelDataService {/*** 保存导入的数据* 分批进入 防止数据过大 - 栈溢出** @param t 保存的数据类型*/@Overridepublic <T> ResultResponse saveExcelData(Integer functionType, List<T> t) {//测试演示(添加数据库)return ResultResponse.booleanToResponse(true);}
//
// /**
// * 获取数据并导出到excel表格中
// *
// * @param t 传入对象
// * @return t类型集合
// */
// @Override
// public <T> List<T> getExcelData(T t) {
// //测试演示
// return null;
// }
}
2.2.3 upload.html测试页面
网上找的前端
页面, 改了改, 自行测试, 我这里没有token传入
位置,
解决方案一: 后端放行一下, 测试后关闭即可
解决方案二: 让前端直接连, 用前端写过的页面
等等
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>EasyExcel</title>
</head><body>
<div class="app"><input type="file" id="fileInput" accept=".xlsx, .xls, .csv"><button onclick="upload()">单sheet上传</button><br><br><input type="file" id="fileInput1" accept=".xlsx, .xls, .csv"><button onclick="upload1()">多sheet上传</button>
</div>
<br>
<div><button onclick="download()">单sheet导出</button> <button onclick="download1()">多sheet导出</button>
</div>
</body><script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>const upload = () => {// 获取文件输入元素const fileInput = document.getElementById('fileInput')// 获取选中的文件const file = fileInput.files[0]if (!file) {alert('请选择一个文件')return}// 创建 FormData 对象const formData = new FormData()// 将文件添加到 FormData 对象formData.append('file', file)// 发送 POST 请求到后端axios.post('http://localhost:8001/system/excel/upload?functionType=1', formData, {headers: {'Content-Type': 'multipart/form-data' // 设置正确的 Content-Type}}).then(response => {alert('文件上传成功')console.log('文件上传成功:', response.data)}).catch(error => {console.error('文件上传失败:', error)});}const upload1 = () => {// 获取文件输入元素const fileInput = document.getElementById('fileInput1')// 获取选中的文件const file = fileInput.files[0]if (!file) {alert('请选择一个文件')return}// 创建 FormData 对象const formData = new FormData()// 将文件添加到 FormData 对象formData.append('file', file)// 发送 POST 请求到后端axios.post('http://localhost:8001/system/excel/upload1?functionType=2', formData, {headers: {'Content-Type': 'multipart/form-data', // 设置正确的 Content-Type'token': ''}}).then(response => {alert('文件上传成功')console.log('文件上传成功:', response.data)}).catch(error => {console.error('文件上传失败:', error)});}const headers = {// 'Content-Type': 'application/json', // 设置请求头部的Content-Type为application/json// token: '', // 设置请求头部的Authorization为Bearer your_token// 'Token4545': '1', // 设置请求头部的Authorization为Bearer your_token// 'responseType': 'blob', // 设置响应类型为blob(二进制大对象)};const download = () => {const url = 'http://192.168.1.254:8001/system/excel/download?fileName=单S文件&functionType=1'axios.get(url, {responseType: 'blob'}).then(response => {// 从Content-Disposition头部中获取文件名const contentDisposition = response.headers['content-disposition']console.log(response)console.log(contentDisposition)const matches = /filename\*=(utf-8'')(.*)/.exec(contentDisposition)console.log(matches)let filename = 'downloaded.xlsx'if (matches != null && matches[2] != null) {console.log(matches[2])// 解码RFC 5987编码的文件名filename = decodeURIComponent(matches[2].replace(/\+/g, ' '))} else {// 如果没有filename*,尝试使用filenameconst filenameMatch = /filename="(.*)"/.exec(contentDisposition);console.log(71)if (filenameMatch != null && filenameMatch[1] != null) {filename = filenameMatch[1]console.log(74)}}// 创建一个a标签用于下载const a = document.createElement('a')// 创建一个URL对象,指向下载的文件const url = window.URL.createObjectURL(new Blob([response.data]))a.href = urla.download = filename // 设置文件名document.body.appendChild(a)a.click()document.body.removeChild(a)window.URL.revokeObjectURL(url)}).catch(error => {console.error('下载文件时出错:', error)})}const download1 = () => {const url = 'http://192.168.1.254:8001/system/excel/test2'axios.get(url, {responseType: 'blob', // 设置响应类型为blob(二进制大对象)}).then(response => {// 从Content-Disposition头部中获取文件名const contentDisposition = response.headers['content-disposition']console.log(response)console.log(contentDisposition)const matches = /filename\*=(utf-8'')(.*)/.exec(contentDisposition)console.log(matches)let filename = 'downloaded.xlsx'if (matches != null && matches[2] != null) {console.log(matches[2])// 解码RFC 5987编码的文件名filename = decodeURIComponent(matches[2].replace(/\+/g, ' '))} else {// 如果没有filename*,尝试使用filenameconst filenameMatch = /filename="(.*)"/.exec(contentDisposition);console.log(71)if (filenameMatch != null && filenameMatch[1] != null) {filename = filenameMatch[1]console.log(74)}}// 创建一个a标签用于下载const a = document.createElement('a')// 创建一个URL对象,指向下载的文件const url = window.URL.createObjectURL(new Blob([response.data]))a.href = urla.download = filename // 设置文件名document.body.appendChild(a)a.click()document.body.removeChild(a)window.URL.revokeObjectURL(url)}).catch(error => {console.error('下载文件时出错:', error)})}
</script></html>
3.业务实战方式与效果(可跳过2.2
)核心
前言: 2.2介绍的是简单的demo
, 根据那个进行拓展
业务需求
- 客户点击- 生成模板, 生成空的excel模板
- 根据说明填写具体信息
- 导入后, 如果数据正常,导入成功
- 导入异常, 则明确告知数据问题在哪
- 本次导入的数据均不生效
- 面对多sheet导入异常, 明确指出
sheet名内的第*条数据,什么问题
, 其他上同操作方式:
- 设置批次导入(发放
唯一批次号
)- 同批次的一组报错
全部回滚
- 导入时生成批次,
整个线程
使用一个批次- 全字段
自定义校验
, 准确定位错误数据,给出精准提示
3.1 业务工具类
3.1.1 ThreadLocalUtils工具类(批次号)
写个基础的set和get , 通过当前线程
传递xid号
,
import java.util.Map;/*** threadLocal使用工具方法* <p>* ps: jdk建议将 ThreadLocal 定义为 private static* 避免: 有弱引用,内存泄漏的问题了** @author pzy* @description TODO beta01测试中* @version 1.0.1*/
public class ThreadLocalUtils {private static final ThreadLocal<Map<String, Object>> mapThreadLocal = new ThreadLocal<>();//获取当前线程的存的变量public static Map<String, Object> get() {return mapThreadLocal.get();}//设置当前线程的存的变量public static void set(Map<String, Object> map) {mapThreadLocal.set(map);}//移除当前线程的存的变量public static void remove() {mapThreadLocal.remove();}
}
3.1.2 自定义字段校验(注解)
-> 3.1.2_1 创建校验注解@DataCheck
如有更细致的校验, 请自行添加
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;/*** 实体类-数据校验注解* <p>* ps: 第一版* 校验方式* 1. 数据为空* 2. 最大长度* 3. 正则表达式* 4. 报错信息* <p>* 其中功能校验在 ValidatorUtils 中** @author pzy* @version 1.0.1* @description ok*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DataCheck {/*** 校验不能为空 true开启 false关闭*/boolean notBank() default false;/*** 长度*/int maxLength() default -1;/*** 正则表达式*/String value() default "";/*** 报错信息*/String message() default "";}
-> 3.1.2_2 注解实现类ValidatorUtils(校验逻辑)
对
@DataCheck
校验逻辑进行支持, 其中异常条数和异常sheet名称
(多sheet需要)需要传递
这里先不管这俩参数
方法一: 单sheet
方法二: 多sheet
import com.alibaba.fastjson.JSON;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;import java.lang.reflect.Field;/*** 校验器工具类*/
@Slf4j
public class ValidatorUtils {/*** DataCheck注册-正则校验器1*/@SneakyThrowspublic static ResultResponse validate(Object obj, Integer errorCounts) {return validate(obj, errorCounts, null);}/*** DataCheck注册-正则校验器2*/@SneakyThrowspublic static ResultResponse validate(Object obj, Integer errorCounts, String sheetName) {Field[] fields = obj.getClass().getDeclaredFields();for (Field field : fields) {if (field.isAnnotationPresent(DataCheck.class)) {DataCheck annotation = field.getAnnotation(DataCheck.class);field.setAccessible(true);Object value = field.get(obj);//实体类参数int maxLength = annotation.maxLength(); //长度String message = "";if (StringUtils.isNotBlank(sheetName)) {message = String.format("可能是: 品类: %s ,第 %s 条,要求: %s", sheetName, errorCounts, annotation.message()); //报错信息} else {message = String.format("可能是: 第 %s 条,要求: %s", errorCounts, annotation.message()); //报错信息}String matchValue = annotation.value();//正则表达式/*校验1: 开启校验 且参数是空的 */if (annotation.notBank() && (value == null || value == "")) {log.warn("Field :[" + field.getName() + "] is null");log.error("校验出异常的数据是:=====> {}", JSON.toJSONString(obj));
// throw new IllegalArgumentException("数据为空呦, " + message);return ResultResponse.error("数据为空呦, " + message);}/*校验2: 长度字段大于0 并且长度大于*/if (maxLength > 0) {if (maxLength < String.valueOf(value).length()) {log.warn("Field :[" + field.getName() + " ] is out of range");log.error("校验出异常的数据是:=====> {}", JSON.toJSONString(obj));
// throw new IllegalArgumentException("数据超范围了呦, " + message);return ResultResponse.error("数据超范围了呦, " + message);}}/*校验3: 正则不匹配 则刨除异常*/if (StringUtils.isNotBlank(matchValue) && value != null && !value.toString().matches(matchValue)) {log.warn("Field :[" + field.getName() + "] is not match");log.error("校验出异常的数据是:=====> {}", JSON.toJSONString(obj));
// throw new IllegalArgumentException("数据格式不对呦, " + message);return ResultResponse.error("数据格式不对呦, " + message);}}}return ResultResponse.ok();}}
3.2 工程内业务使用
3.2.0 创建上传或下载对象dto
添加校验注解 excel注册等, 不可使用@Accessors注解
/*** 临时客户dto** @author pzy* @version 0.1.0* @description ok*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserTempDTO {@DataCheck(notBank = true, maxLength = 255, value = "[A-Za-z0-9_\\-\\u4e00-\\u9fa5]+", message = "(非空)用户姓名支持中文,英文,数字,'-' 和'_', 长度255位")@ExcelProperty(value = "真实姓名")private String realname;@DataCheck(maxLength = 2, message = "性别请填写: 男,女,未知")@ExcelProperty(value = "性别")private String gender;@DataCheck(notBank = true,maxLength = 255, value = "0?(13|14|15|18|17)[0-9]{9}", message = "(非空)手机号需纯数字且长度11位")@ExcelProperty(value = "电话号")private String phone;// @DataCheck(maxLength = 255, value = "[A-Za-z0-9_\\-\\u4e00-\\u9fa5]+", message = "地址信息名称支持中文,英文,数字,'-' 和'_', 长度255位")@DataCheck(maxLength = 255, message = "地址信息名称长度255位")@ExcelProperty(value = "地址信息")private String familyAddr;// @DataCheck(maxLength = 255, value = "[A-Za-z0-9_\\-\\u4e00-\\u9fa5]+", message = "头像链接地址,长度255位")@DataCheck(maxLength = 255, message = "头像链接地址,长度255位")@ExcelProperty(value = "头像")private String avatarUrl;//---------------------------------------->@ExcelIgnore@ExcelProperty(value = "备用电话号")private String sparePhone;@ExcelIgnore@ExcelProperty(value = "昵称")private String nickname;@ExcelIgnore@ApiModelProperty(value = "excel的sheet名称")private String sheetName;@ExcelIgnore@ApiModelProperty(value = "excel的sheet名称对应行号,用于报错行数")private String sheetDataNo;@ExcelIgnore@ApiModelProperty(value = "xid号")private String xid;}
测试校验是否生效
public static void main(String[] args) {UserTempDTO userTempDTO = new UserTempDTO();userTempDTO.setRealname("");userTempDTO.setGender("男");userTempDTO.setPhone("14788888888");userTempDTO.setFamilyAddr("");userTempDTO.setAvatarUrl("");ValidatorUtils.validate(userTempDTO,10);}
3.2.1 创建controller
业务的入口
@Slf4j
@RequiredArgsConstructor
@RestController
@RequestMapping("/excel/test")
public class SystemExcelController {private final SystemExcelService systemExcelService;@PostMapping("/upload")public ResultResponse upload(MultipartFile file, ExcelReqDTO excelReqDTO) throws IOException {log.info("===> excel文件上传 <===");//文件校验UploadCheckUtils.uploadVerify(file);try {Map<String, Object> map = new HashMap<>();long snowId = IdGenerater.getInstance().nextId();log.info("excel导入e_xid===> {}",snowId);map.put("e_xid", snowId);//存入threadLocalThreadLocalUtils.set(map);systemExcelService.upload(file, excelReqDTO);} finally {ThreadLocalUtils.remove();}return ResultResponse.ok("操作成功");}@GetMapping("/download")public void download(HttpServletResponse httpServletResponse, ExcelReqDTO excelReqDTO) throws IOException {log.info("===> excel文件下载 <===");systemExcelService.download(httpServletResponse, excelReqDTO);}}
3.2.2 接口SystemExcelService
/*** excel表格实现类* @author pzy* @version 0.1.0* @description ok*/
public interface SystemExcelService {/*** 上传excel文件* @param file 文件* @param excelReqDTO 请求参数*/void upload(MultipartFile file, ExcelReqDTO excelReqDTO);void download(HttpServletResponse httpServletResponse, ExcelReqDTO excelReqDTO);
}
3.2.3 实现类SystemExcelServiceImpl(需根业务自行调整)
这里面就是具体业务了
使用了ExcelUtils方法 实现多/单sheet导入与导出
导入ps:
在使用excelUtils方法时, 需要注入ExcelDataService接口来实现数据库存储操作
导出ps:
查询数据库数据, 处理 传入Lists.newArrayList() 这个位置即可
/*** excel表格实现类** @author pzy* @version 0.1.0* @description ok*/
@Service
@Slf4j
@RequiredArgsConstructor
public class SystemExcelServiceImpl implements SystemExcelService {private final ExcelDataService excelDataService;/*** 上传excel功能文件** @param file 文件* @param excelReqDTO 请求参数*/@SneakyThrows@Overridepublic void upload(MultipartFile file, ExcelReqDTO excelReqDTO) {//功能类型 1 2 3Integer functionType = excelReqDTO.getFunctionType();if (Objects.equals(functionType, 1)) {//多sheetExcelUtils.readExcel(file.getInputStream(),*.class,new ExcelDataListener<>(excelDataService, functionType),MathUtils.getIntRangeToList(0, 8));} else if (Objects.equals(functionType, 2)) {////单sheetExcelUtils.readExcel(file.getInputStream(),*.class,new ExcelDataListener<>(excelDataService, functionType), 0);} else if (Objects.equals(functionType, 3)) {////单sheetExcelUtils.readExcel(file.getInputStream(),*.class,new ExcelDataListener<>(excelDataService, functionType), 0);} else {throw new ServiceException(ResponseEnum.E30001);}}@SneakyThrows@Overridepublic void download(HttpServletResponse httpServletResponse, ExcelReqDTO excelReqDTO) {String fileName = excelReqDTO.getFileName();if (StringUtils.isBlank(fileName) || fileName.length() > 6) {throw new ServiceException("抱歉名称长度需大于0且不能超过6呦~");}//功能类型 1 2 3Integer functionType = excelReqDTO.getFunctionType();if (Objects.equals(functionType, 1)) {//sheet名字List<String> sheetNameList = ?;List<List<*>> sheetDataList = Lists.newArrayList();sheetNameList.forEach(sheetDto->sheetDataList.add(Lists.newArrayList()));ExcelUtils.complexDownload(httpServletResponse, fileName,ShopOfflineListDTO.class, sheetNameList,sheetDataList,new LongestMatchColumnWidthStyleStrategy(),ExcelStyleTool.getStyleStrategy());} else if (Objects.equals(functionType, 2)) {////写出excel核心代码ExcelUtils.easyDownload(httpServletResponse,fileName,*.class,"模板1",Lists.newArrayList(),//需要数据就传入 不需要就传递空集合new LongestMatchColumnWidthStyleStrategy(),ExcelStyleTool.getStyleStrategy());} else if (Objects.equals(functionType, 3)) {//写出excel核心代码ExcelUtils.easyDownload(httpServletResponse,fileName,*.class,"模板1",Lists.newArrayList(),new LongestMatchColumnWidthStyleStrategy(),ExcelStyleTool.getStyleStrategy());} else {throw new ServiceException(ResponseEnum.E30001);}}
}
3.2.4 寻找ExcelDataService的实现类
选择自己工程下的实现类, 写3.2.3的具体业务
如遇问题请提出
实现类重写saveExcelData()方法, 这里就列举其中的两种使用方式, 业务代码跳过
/*** 保存导入的数据* 分批进入 防止数据过大 - 栈溢出** @param t 保存的数据类型*/
// @Transactional@Overridepublic <T> ResultResponse saveExcelData(Integer functionType, List<T> t) {MemberResponseVo user = AuthServerConstant.loginUser.get();int companyId = user.getCompanyId();log.info("需要保存的数据: {}", JSON.toJSONString(t));//获取当前xid号-批次号(数据安全)String eXid = String.valueOf(ThreadLocalUtils.get().get("e_xid"));log.info("业务中: e_xid号=========================> {}", eXid);//功能类型 1配件(库存) 2供应商 3客户(假)资料if (Objects.equals(functionType, 1)) {//1return upload111Data(t, companyId, eXid);} else if (Objects.equals(functionType, 2)) {//2return upload222Data(t, companyId, eXid);} else if (Objects.equals(functionType, 3)) {//3return upload333Data(t, companyId, eXid);} else {throw new ServiceException(ResponseEnum.E30001);}}/*** 1. 上传配件数据** @param t 传入数据* @param companyId 公司id* @param eXid eXid* @return ResultResponse对象*/private <T> ResultResponse uploadPartsData(List<T> t, Integer companyId, String eXid) {List<***> a1List;try {a1ListList = JSON.parseObject(JSON.toJSONString(t), new TypeReference<List<***>>() {});} catch (Exception e) {e.printStackTrace();return ResultResponse.error("类型不匹配,请先检查金额字段,必须是纯数字的整数或小数哟~");}if (CollectionUtils.isEmpty(a1List)) {return ResultResponse.ok("无数据需要导入~");}//数据处理a1List.forEach(a1DTO -> {//数据校验ResultResponse response = ValidatorUtils.validate(a1DTO, Integer.valueOf(a1.getSheetDataNo()), a1.getSheetName());if (!ResponseHelper.judgeResp(response)) {//执行回滚操作if (!ResponseHelper.judgeResp(productFeignService.rollBackPartsData(eXid))) {log.error("======> 数据eXid: {} 回滚失败了 ", eXid);}throw new IllegalArgumentException(response.getMsg());}a1.setSourceType(1);a1.setXid(eXid);//根据品类名称 转换成品类ida1.setTypeId(changeTypeNameToId(a1.getSheetName()));});//远程调用 即使出现问题也不会滚 业务内直接删除数据重新传递return ***.saveBatch(a1List);}
客户导入, 这个保留业务代码 方便查看具体使用方式
/*** 3. 上传客户临时数据** @param t 传入数据* @param companyId 公司id* @param eXid eXid* @return ResultResponse对象*/private <T> ResultResponse uploadUserTempData(List<T> t, Integer companyId, String eXid) {List<UserTempDTO> userTempList = JSON.parseObject(JSON.toJSONString(t), new TypeReference<List<UserTempDTO>>() {});if (CollectionUtils.isEmpty(userTempList)) {return ResultResponse.ok("无数据需要导入呦~");}List<AxUserTemp> axUserTempList = userTempList.stream().map(userTempDTO -> {//数据校验(包含回滚)ResultResponse response = ValidatorUtils.validate(userTempDTO, Integer.valueOf(userTempDTO.getSheetDataNo()));if (!ResponseHelper.judgeResp(response)) {rollBackAxUserTemp(eXid);throw new IllegalArgumentException(response.getMsg());}AxUserTemp axUserTemp = new AxUserTemp();BeanUtils.copyProperties(userTempDTO, axUserTemp);axUserTemp.setId(IdGenerater.getInstance().nextId()).setUserRole(UserRoleEnum.CONSUMER.getCode()).setCompanyId(companyId).setCreateTime(DateUtils.getNowDate()).setDelFlag(1).setXid(eXid);return axUserTemp;}).collect(Collectors.toList());try {if (!SqlHelper.retBool(axUserTempMapper.insertBatchSomeColumn(axUserTempList))) {rollBackAxUserTemp(eXid);throw new SystemServiceException(ResponseEnum.E500, String.format("前 %s 条数据存在问题,数据导入失败", axUserTempList.size()));}} catch (DuplicateKeyException e) {e.printStackTrace();rollBackAxUserTemp(eXid);throw new SystemServiceException(ResponseEnum.E500, String.format("前 %s 条数据重复,请检查(可能重复提交)", axUserTempList.size()));} catch (Exception e) {e.printStackTrace();rollBackAxUserTemp(eXid);throw new SystemServiceException(ResponseEnum.E500, String.format("前 %s 条数据存在问题,数据导入异常", axUserTempList.size()));}return ResultResponse.ok();}
其中rollbackAxUserTemp()方法如下, 手动提交事务
第一步:注入事务管理器
/*** 事务管理器*/private final PlatformTransactionManager platformTransactionManager;/*** 事务的一些基础信息,如超时时间、隔离级别、传播属性等*/private final TransactionDefinition transactionDefinition;
第二步: 根据
xid号进行删除数据
代表回滚, 添加代码 (其中可以添加一些参数 我这直接默认了)
/*** 回滚临时用户数据(调用-事务不看结果直接提交)** @param eXid xid号*/private void rollBackAxUserTemp(String eXid) {TransactionStatus transaction = platformTransactionManager.getTransaction(transactionDefinition);//TransactionStatus : 事务的一些状态信息,如是否是一个新的事务、是否已被标记为回滚try {axUserTempMapper.delete(Wrappers.<AxUserTemp>lambdaQuery().eq(AxUserTemp::getXid, eXid));platformTransactionManager.commit(transaction);} catch (Exception e) {// 回滚事务platformTransactionManager.rollback(transaction);throw e;}}
3.3 程序测试执行结果及报错解决
3.3.1 执行结果
前端接入, 可以根据上面
testGroup里面html
的进行调整
后端部署, 测试, 效果如下
3.3.2 报错解决
emm, 代码太长了, 遇到,想用的话评论或私信吧, 遇到的问题太多了,
挑几个重点的
3.3.2_1 CROS跨域问题
- 生产环境跨域, 代理一下,配置nginx
- 开发环境: 本地开跨域只能解决其中一种问题, 下个
插件cros
就行了 , 有更好的办法(后端
)欢迎评论哈~
3.3.2_2 excel表格导出是空
去掉@Accessors(chain = true)即可
3.3.2_3 导入dto中有list报错
使用注解 @ExcelProperty(value = “”,converter = MyConverter.class)
试一下, 不好用评论区发一下
3.3.2_4 导出模板/sheet的名字不正确
基本是前端的问题了, 按照html里去改即可
3.3.2_5 待续未完…
想不起来还遇到哪些问题了, 业务层面的不包含, 多线程测试也正常, 等遇到问题在调整本文
如遇到部分类没有, 可根据上下文行为自行更改或评论区指出
逐步在这里
添加
4. 文章的总结与预告
4.1 本文总结
easyExcel实现具体操作, 遇到问题请看 3.3
4.2 下文预告
暂无
@author: pingzhuyan
@description: ok
@year: 2024
相关文章:
EasyExcel: 结合springboot实现表格导出入(单/多sheet), 全字段校验,批次等操作(全)
全文目录,一步到位 1.前言简介1.1 链接传送门1.1.1 easyExcel传送门 2. Excel表格导入过程2.1 easyExcel的使用准备工作2.1.1 导入maven依赖2.1.2 建立一个util包2.1.3 ExcelUtils统一功能封装(单/多sheet导入)2.1.4 ExcelDataListener数据监听器2.1.5 ResponseHelper响应值处理…...
志愿者小程序源码社区网格志愿者服务小程序php
志愿者服务小程序源码开发方案:开发语言后端php,tp框架,前端是uniapp。 一 志愿者端-小程序: 申请成为志愿者,志愿者组织端进行审核。成为志愿者后,可以报名参加志愿者活动。 志愿者地图:可以…...
HTML实现 扫雷游戏
前言: 游戏起源与发展 扫雷游戏的雏形可追溯到 1973 年的 “方块(cube)” 游戏,后经改编出现了 “rlogic” 游戏,玩家需为指挥中心探出安全路线避开地雷。在此基础上,开发者汤姆・安德森编写出了扫雷游戏的…...
小白学多线程(持续更新中)
1.JDK中的线程池 JDK中创建线程池有一个最全的构造方法,里面七个参数如上所示。 执行流程分析: 模拟条件:10个核心线程数,200个最大线程数,阻塞队列大小为100。 当有小于十个任务要处理时,因为小于核心线…...
【uni-app多端】修复stmopjs下plus-websocket无心跳的问题
从这篇文章接着向下看: uniapp plus-websocket 和stompjs连接教程 安卓ios手机端有效 - 简书 按照文章的方式,能够实现APP下stmopjs长连接。但是有一个问题,就是会频繁输出 res-创建连接-1- 跟踪连接,会发现连接都会在大约40s后…...
【SLAM文献阅读】基于概率模型的视觉SLAM动态检测与数据关联方法
A dynamic detection and data association method based on probabilistic models for visual SLAM 《基于概率模型的视觉SLAM动态检测与数据关联方法》 2024 摘要: 通常,静态特征采用多视图几何来估计相机姿态和重建环境地图。因此,动态特…...
Linux系统使用valgrind分析C++程序内存资源使用情况
内存占用是我们开发的时候需要重点关注的一个问题,我们可以人工根据代码推理出一个消耗内存较大的函数,也可以推理出大概会消耗多少内存,但是这种方法不仅麻烦,而且得到的只是推理的数据,而不是实际的数据。 我们可以…...
Selenium+Java(19):使用IDEA的Selenium插件辅助超快速编写Pages
前言 或是惊叹于Selenium对于IDEA的支持已经达到了这样的地步,又或是由于这个好用的小工具的入口就在那里,它已经陪伴了我这么久,而我这么久的时间却都没有发现它。在突然发现这个功能的一瞬间,真的是喜悦感爆棚,于是赶快写下了这篇文章。希望可以帮助到其他同样在做UI自动…...
Unity 设计模式-单例模式(Singleton)详解
设计模式 设计模式 是指在软件开发中为解决常见问题而总结出的一套 可复用的解决方案。这些模式是经过长期实践证明有效的 编程经验总结,并可以在不同的项目中复用。设计模式并不是代码片段,而是对常见问题的 抽象解决方案,它提供了代码结构…...
OAuth协议详解
一、基本概念 OAuth(Open Authorization)是一种授权协议,用于允许第三方应用程序以受信任的方式访问用户的资源,而无需共享用户的身份验证凭据。OAuth协议的核心目标是在保持用户数据安全的前提下,简化用户在不同应用…...
2024收官之战:车展向下,智驾向上
作者 | 德新 编辑 | 王博 广州车展在上周拉开帷幕,在激烈的车市竞争中,可以说没有一届车展比本届更加「 冰火两重天」。 在本届车展前夕,已经传出不少车企集团面临业务整合的消息,一部分品牌缺席了本届车展,而势头强…...
开源项目-如何更好的参与开源项目开发
开源之谜-提升自我核心竞争力 一、寻找适合自己的开源项目二、像坐牢一样闭关修炼三、最后的实践 开源代码对所有人开放,开发者可以基于现有代码进行扩展和创新,而不是从零开始,参与开源项目可以提升自我的技术能力,丰富个人的经历…...
如何重命名 Conda 环境 - 详细教程
如何重命名 Conda 环境 - 详细教程 前言重命名步骤1. 克隆现有环境2. 验证新环境3. 删除旧环境 实例演示注意事项常见问题解答Q1: 为什么 Conda 没有直接的重命名命令?Q2: 重命名过程会影响环境中的包吗?Q3: 如果克隆过程中断,会怎么样&#…...
自动驾驶之激光雷达
这里写目录标题 1 什么是激光雷达2 激光雷达的关键参数3 激光雷达种类4 自动驾驶感知传感器5 激光雷达感知框架5.1 pointcloud_preprocess5.2 pointcloud_map_based_roi5.3 pointcloud_ground_detection5.4 lidar_detection5.5 lidar_detection_filter5.6 lidar_tracking 1 什么…...
Python毕业设计选题:基于python的豆瓣电影数据分析可视化系统-flask+spider
开发语言:Python框架:flaskPython版本:python3.7.7数据库:mysql 5.7数据库工具:Navicat11开发软件:PyCharm 系统展示 系统首页 个人中心 管理员登录界面 管理员功能界面 电影管理 用户管理 系统管理 摘要…...
从 Mac 远程控制 Windows:一站式配置与实践指南20241123
引言:跨平台操作的需求与挑战 随着办公场景的多样化,跨平台操作成为现代开发者和 IT 人员的刚需。从 Mac 系统远程控制 Windows,尤其是在同一局域网下,是一种高效解决方案。不仅能够灵活管理资源,还可以通过命令行简化…...
k8s部署Nginx详细教程
Kubernetes(简称k8s)是一个开源的容器编排平台,用于自动化部署、扩展和管理容器化应用程序。本文将详细介绍如何使用k8s部署Nginx,包括创建部署配置、创建服务以及如何通过一个命令完成部署和删除。 环境准备 在开始之前&#x…...
PySpark3:pyspark.sql.functions常见的60个函数
目录 一、常见的60个函数 1、col 2、lit 3、sum 4、avg/mean 5、count 6、max 7、min 8、concat 9、substring 10、lower 11、upper 12、trim 13、ltrim 14、rtrim 15、split 16、explode 17、collect_list 18、collect_set 19、asc 20、desc 21、when 2…...
网络爬虫总结与未来方向
通过深入学习和实际操作,网络爬虫技术从基础到进阶得以系统掌握。本节将全面总结关键内容,并结合前沿技术趋势与最新资料,为开发者提供实用性强的深度思考和方案建议。 1. 网络爬虫技术发展趋势 1.1 趋势一:高性能分布式爬虫 随…...
【优先算法】专题——双指针
1.移动零 移动零 题目描述: 思路: 本题我们把数组分块,将非零元素移动到左边,为零元素移动右边。 我们使用双指针算法(利用数组下标来充当指针) 两个指针的作用: cur:从左往右…...
互联网时代的隐私保护
在这个数字化时代,我们的生活与互联网密不可分。打开手机刷刷朋友圈,浏览一下购物网站,约个网约车,点个外卖,这些看似平常的行为都在默默产生着数据足迹。可就在这不经意间,我们的个人信息正在被收集、分析…...
活着就好20241124
今天是周日,一个同样洋溢着休闲与宁静气息的日子。亲爱的朋友们,大家早上好!在经历了一周的忙碌之后,我们终于迎来了这个让人期待已久的休息日。周日,不仅是一个放松身心的绝佳时机,更是我们回归自我、享受…...
镁光MT25QU01GXXX norflash调试笔记
目录 前言一、芯片概述二、数据手册解释1. 数据手册获取2.内容概括 三、几个操作的代码1.复位芯片操作2.读取芯片ID3.擦除芯片扇区4.向芯片存入数据5.读取存储的数据6.其它操作函数 前言 本笔记总结如何使用MCU对nor flash进行数据存储,包括芯片基本介绍࿰…...
并行IO接口8255
文章目录 8255A芯片组成外设接口三个端口两组端口关于C口(★) 内部逻辑CPU接口 8255A的控制字(★)位控字(D70)方式选择控制字(D71) 8255A的工作方式工作方式0(基本输入/输…...
[Unity Demo]从零开始制作空洞骑士Hollow Knight第二十集:制作专门渲染HUD的相机HUD Camera和画布HUD Canvas
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 前言一、制作HUD Camera以及让两个相机同时渲染屏幕二、制作HUD Canvas 1.制作法力条Soul Orb引入库2.制作生命条Health读入数据3.制作吉欧统计数Geo Counter4.制作…...
《AI大模型开发笔记》——Prompt提示词最佳实践
为什么需要提示词优化 什么是 Prompt enginnering? 提示词:解释一下什么是大语言模型的Prompt enginnering 图1. 什么是Prompt Enginnering? 通过上面ChatGPT的说明,我们可以看到,prompt工程化就是如何写prompt提示…...
Frontend - 防止多次请求,避免重复请求
目录 一、避免重复执行的多种情况 (一)根据用途 (二)根据用户操作 二、具体实现 (一)“Ajax ”结合disabled (防止多次请求),避免多次点击重复请求 1. 适用场景 2. 解决办法 3. 示例 &…...
MongoDB 简介
MongoDB 简介 MongoDB 是一种流行的 NoSQL 数据库管理系统,以其灵活的数据模型、高性能和易于扩展的特点而闻名。本文将详细介绍 MongoDB 的基本概念、特点、使用场景以及如何在实践中应用。 1. MongoDB 基本概念 1.1 文档数据库 MongoDB 是一个文档数据库,它使用 JSON 风…...
Ngrok实现内网穿透(Windows)
Ngrok实现内网穿透(Windows) 什么是内网穿透,内网穿透有什么用 内网穿透(NAT traversal)是一种技术手段,使得位于内网或防火墙后面的设备能够通过外网访问。例如,如果你的计算机、服务器等设备…...
【Python-办公自动化】实现自动化输出模板表格报告
import pandas as pd import numpy as np# 定义时间范围 date_range = pd.date_range(start=2023-11-01, end=2024-10-31, freq=M...
医学AI公开课·第一期|Machine LearningTransformers in Med AI
小罗碎碎念 从这周开始,我计划每个周末录一个视频,分享一些医学人工智能领域的进展。 作为第一期视频,我打算介绍一下机器学习和Transformer在医学AI领域中的应用。 为了准备这期视频,总共做了24页PPT(三部分内容&…...
ESP8266 STA模式TCP服务器 电脑手机网络调试助手
STA模式TCP服务器和手机电脑网络调试助手多连接...
Transformer架构笔记
Attention is All You Need. 3.Model Architecture 3.1 整体架构如图 3.2 Encoder与Decoder Encoder:由 N 6 N6 N6个相同的Block/Layer堆叠而成。每个Block有两个子层sub-layer:多头注意力和MLP(FFN,前馈神经网络)&…...
初学 flutter 环境变量配置
一、jdk(jdk11) 1)配置环境变量 新增:JAVA_HOMEC:\Program Files\Java\jdk-11 //你的jdk目录 在path新增:%JAVA_HOME%\bin2)验证是否配置成功(cmd运行命令) java java -version …...
【大数据技术基础】 课程 第8章 数据仓库Hive的安装和使用 大数据基础编程、实验和案例教程(第2版)
第8章 数据仓库Hive的安装和使用 8.1 Hive的安装 8.1.1 下载安装文件 访问Hive官网(http://www.apache.org/dyn/closer.cgi/hive/)下载安装文件apache-hive-3.1.2-bin.tar.gz 下载完安装文件以后,需要对文件进行解压。按照Linux系统使用的…...
Postman之newman
系列文章目录 1.Postman之安装及汉化基本使用介绍 2.Postman之变量操作 3.Postman之数据提取 4.Postman之pm.test断言操作 5.Postman之newman Postman之newman 1.基础环境node安装1.1.配置环境变量1.2.安装newman和html报告组件 2.newman运行 newman可以理解为,没有…...
Lua 实现继承的一种方式
以下代码来自Loxodon Framework,截取自其中的一段 function class(classname, super)local cls {}cls.__classname classnamecls.__class clscls.base function (self)return cls.superendcls.__type 0cls.super supercls.__index clsif super thensetmetat…...
相机网卡开启巨型帧和关闭节能模式方法
2022 年 8 月 2 日 Tank 阅读次数(ip/1年): 26,796 win10为例子 首先在开始菜单搜索:网络连接 对想要设置的网络右键:属性 点 配置 高级里面找到这三个选项,参考下图设置,螃蟹网卡建议关掉所有节能有关的…...
如何在 Ubuntu 22.04 上安装带有 Nginx 的 ELK Stack
今天我们来聊聊如何在 Ubuntu 22.04 服务器上安装 ELK Stack,并集成 Nginx 作为 Web 服务器,同时使用 Let’s Encrypt Certbot 进行 SSL 认证。ELK Stack,包括 Elasticsearch、Logstash 和 Kibana,是一套强大的工具,用…...
Android中的依赖注入(DI)框架Hilt
Hilt 是 Android 提供的一种依赖注入(DI)框架,它基于 Dagger,目的是简化依赖注入的使用,提供更易用的接口和与 Android 生命周期组件的紧密集成。下面是 Hilt 的详细介绍。 为什么选择 Hilt? 依赖注入的优势…...
笔记记录 k8s操作
docker下载arm架构的镜像 docker pull centos --platform arm64 其中华为鲲鹏、飞腾CPU采用的是ARM架构,龙芯采用的是MIPS架构,而兆芯、海光CPU采用的是X86架构,申威采用的是Alpha架构 docker查看容器的日志文件目录 docker inspect --format={{.LogPath}} containername…...
掌握Go语言中的异常控制:panic、recover和defer的深度解析
掌握Go语言中的异常控制:panic、recover和defer的深度解析 在Go语言的编程世界中,异常处理是一个不可忽视的话题。Go语言提供了panic、recover和defer三个关键字来处理程序中的异常情况。本文将深入探讨这三个关键字的工作原理、使用场景和最佳实践,帮助读者在实际编程中更…...
SpringBoot项目部署到云服务器全流程
文章目录 一、前期准备(一)云服务器选择(二)本地环境准备(三)数据库准备(若项目需要) 二、服务器配置(一)获取服务器信息(二)重置实例…...
对传统加密算法降维打击?!——量子计算
量子计算 声明! 学习视频来自B站up主 泷羽sec 有兴趣的师傅可以关注一下,如涉及侵权马上删除文章,笔记只是方便各位师傅的学习和探讨,文章所提到的网站以及内容,只做学习交流,其他均与本人以及泷羽sec团队无…...
什么是Lodash,有什么特点
什么是 Lodash? Lodash 是一个 JavaScript 工具库,提供了一系列实用的函数来处理常见的编程任务,如数组操作、对象操作、字符串处理等。Lodash 由 John-David Dalton 创建,旨在提供一个更全面、更高效的替代方案,以弥…...
Simulink中Model模块的模型保护功能
在开发工作过程中,用户为想要知道供应商的开发能力,想要供应商的模型进行测试。面对如此要求,为了能够尽快拿到定点项目,供应商会选择一小块算法或是模型以黑盒的形式供客户测试。Simulink的Model模块除了具有模块引用的功能之外&…...
【电子通识】LED的一些基础知识
什么是LED LED是被称为"发光二极管"的半导体,名称取至 "Light Emitting Diode" 的首字母。 从爱迪生1879年发明白炽灯后,白炽灯统治了照明一百多年,为世界带来光明。而在21世纪,白炽灯却早已被取代࿰…...
React Native 基础
React 的核心概念 定义函数式组件 import组件 要定义一个Cat组件,第一步要使用 import 语句来引入React以及React Native的 Text 组件: import React from react; import { Text } from react-native; 定义函数作为组件 const CatApp = () => {}; 渲染Text组件...
C语言 蓝桥杯某例题解决方案(查找完数)
蓝桥杯原题: 一个数如果恰好等于它的因子之和,这个数就称为“完数”。例如6 1 2 3.编程找出1000以内的所有完数。 这个题没有很大的难点,与我们上一个解决的问题“质因数分解”不同,它不需要判断因数是否是质数,因此…...
【shodan】(三)vnc漏洞利用
shodan基础(三) 声明:该笔记为up主 泷羽的课程笔记,本节链接指路。 警告:本教程仅作学习用途,若有用于非法行为的,概不负责。 count count命令起到一个统计计数的作用。 用上节的漏洞指纹来试…...