⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 juejin.cn/post/7222676935147651132 「云雨雪」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

前言

本文的选题来源于一周前我的一次崩溃,当时各种压力和工作把我整的不行了,让我在短时间内改另一个同事写的代码。本来以为是一次简单的叠叠乐,没想到随便看了眼之前的代码,发现历史需求没写完,而且逻辑还有错,当时直接蚌埠住了。我的 IDEA 比较新,平时看着报错我都会去手动改一下,有时候我看着同事的代码一片黄,总是忍不住要改。久而久之也烦了,不明白为什么这么明显的 BUG 还能放在上面,于是产生了来一期盘点的想法。排名不分先后,上榜各凭本事,客官们,上菜咯!

案例一

用不明白的三元表达式能不能别用了啊,求求啦。当时看到这个地方的时候,我一眼就觉得很怪,因为我觉得他写错了,但同时我又能了解开发者的意图,这种感觉就很奇妙,我觉得我融入这个 BUG 了。

错误示例,这里我单独写了个例子来证明他真的写错了,和我想得一样,就是执行顺序的问题。

正确示例,加括号,注意执行顺序,这样就对了嘛。

案例二

参数校验真的是非常常见的场景了,有用 AOP 的。

还有方法里 if 判断的。

有简单的法子,他是真不用啊,注解难道不是又快又简洁吗?这还是我开发文档里写了的,我麻了,真就不看文档呗。

案例三

log 日志老是玩不对的,我也是见太多了,像这种占位符写不明白的,不是很理解。

莫名其妙的,我发现好多人喜欢这么写,不看 API 吗,这时候想起占位符了。

传世经典之异常不打印堆栈信息,我觉得这个倒是小问题,或许有别的想法呢,但是我推荐还是记录一下比较好。

案例四

意义不明的代码,要么删除要么打上注释,不要放在那里影响判断。这个问题在我去接手或者修改别人代码的时候最严重,依托答辩放在那里,没有引用也没有后续的使用,那到底是干啥的呢?我是真的猜不透啊,不敢动了。

案例五

严禁在方法内部自由输出错误码,同时也禁止在 service 层输出结果类,这样 controller 层将没有意义,切点在 controller 层的切面会失效。

异常需使用指定异常或者继承了指定异常的异常类抛出,这样才能抓到错误码,否则将被全局监听覆盖错误码和错误信息

同时也不建议在有框架保底的情况下在 controller 层写过多的代码,这种 try catch 能用全局异常监听能处理的就不要自己写,用好异常和错误码即可。

正常像这样简洁方便的写就好了,在 service 层处理的时候抛出自定义的异常即可。这个写法是两年前我做的一次优化,本以为不会出现类似问题了,结果最近的代码中又发现了,真是不改嗷。

案例六

经典自调用切面失效场景,这个其实注意一下就可以避免的,但是很多时候就是写着写着就忘了,非得报错才知道写错了。

最便捷的方法就是注入本体,再调用。

案例七

在使用 Redis 等中间件相关的操作框架比如 Redisson 时,要特别注意不要让中间件宕机时影响程序的正常运行。最近 Redis 宕机了,猛然发现这个问题,想了下确实也不能怪框架,毕竟没有数据和报错确实是两码事,这个只能开发者自己控制。

@Autowired
private RedissonClient redissonClient;
//原始版本
public BigDecimal getIntervalQty(int itemId, Date startDate, Date endDate) {
String cacheKey = "dashboard:intervalQty:" + itemId + "-" + startDate + "-" + endDate;
RBucket<BigDecimal> bucket = redissonClient.getBucket(cacheKey);
BigDecimal cacheValue = bucket.get();
if (cacheValue != null) {
return cacheValue;
} else {
BigDecimal intervalQty = erpInfoMapper.getIntervalQty(itemId, startDate, endDate);
BigDecimal res = Optional.ofNullable(intervalQty).orElse(BigDecimal.valueOf(0)).setScale(2,
RoundingMode.HALF_UP);
bucket.set(res, 16, TimeUnit.HOURS);
return res;
}
}
//更新避免Redis报错版本
public String getProductLine(String itemNo) {
String cacheKey = "order:getProductLine:" + itemNo;
String cacheValue = null;
RBucket<String> bucket = redissonClient.getBucket(cacheKey);
try {
cacheValue = bucket.get();
} catch (Exception e) {
log.error("redis连接异常", e);
}
if (cacheValue != null) {
return cacheValue;
} else {
String res = ptmErpMapper.getProductLine(itemNo);
bucket.set(res, 16, TimeUnit.HOURS);
return res;
}
}

案例八

这是我写的业务组件的一个 BUG,是一个上传文件导入时会报文件找不到的问题,正常我是让大家这么写 Excel 文件上传。

通用Controller层
@LimitMethod
@PostMapping("/import")
public RemoteResult<String> importAdd(@RequestParam("file") MultipartFile multipartFile)

1. 第一步生成单号或者标识,userId之类的最好也取出来,做好传递的准备

identifier标识可使用LuaTool生成
String generateOrder = luaTool.generateOrder("SMB-PRODUCT-");
生成局部变量方便线程间数据传递
RequestContext.getCurrentContext()或者使用CurrentUserUtil工具类(sso-zero提供)

2. 第二步调用commonImportExcel方法读取并传递文件到PTM2.0(这一步必须放在外面,是对excel的基本校验,有错误及时推送前端,不能异步)

该方法包含对excel的基本校验,并且自带上传文件服务器以及传递PTM
long ptmFileId = excelTool.commonImportExcel(file, generateOrder);

3. 开启异步,使用readFile或者readMultipartFile解析文件,并进行业务处理
如果此部分需要事务,请另起一个事务类,使用@Transactional(rollbackFor = Exception.class)或者在当前代码区域手动开启事务或者自注入再调用方法。

CompletableFuture.runAsync(() -> {
读取文件,readMultipartFile方法会调用Easy Excel解析读取excel,如果有读取错误会抛出错误,方法入参中有表头校验,选择true会校验表头是否正确,不正确会抛异常
CustomizedExcelListener excelListener = new CustomizedExcelListener();
EasyExcel.read(finalInputStream, MaterialChipImportDTO.class, excelListener).sheet().doRead();
ExcelAnalyzeResDTO analyzeRes = excelListener.getExcelData();
List<MaterialChipImportDTO> judgeMaterialList = (List<MaterialChipImportDTO>) analyzeRes.getExcelDataList();
//业务处理,这里MaterialChipImportDTO导入类需要冗余一个异常信息字段errMsg,业务处理的时候把错误信息塞进去
List<MaterialChipImportDTO> afterList = judgeChipImport(judgeMaterialList, beforeAllList, isAdd);
//判断errMsg字段是否有值,有值说明这一行有业务逻辑错误
MaterialChipImportDTO orElse = judgeMaterialList.stream()
.filter(ma -> StringUtils.isNotBlank(ma.getErrMsg())).findAny().orElse(null);
//可选,异常文件导出
if (orElse != null) {
String fileName = "错误提示文件-" + finalGenerateOrder;
excelTool.synchronizeExportExcel(judgeMaterialList, MaterialChipImportDTO.class, fileName, fileName,userId, userName);
throw new PtmException("文件校验有错误项,请下载错误提示文件");
} else {
//继续业务操作
}
}
})

4. handle处理部分,调用finishFileStatus方法回传PTM2.0状态,注意这里的ptmFileId是指PTM文件列表的id
.handle((res, e) -> {
if (e != null) {
log.error("物料风险地图芯片导入异步处理数据失败,流程单号:{},异常信息:", generateOrder, e);
excelTool.finishFileStatus(ptmFileId, null, null, ExcelFieldConstant.TYPE_IMPORT,
ExcelFieldConstant.IMPORT_FAILED, e.getMessage());
} else {
excelTool.finishFileStatus(ptmFileId, null, null, ExcelFieldConstant.TYPE_IMPORT,ExcelFieldConstant.IMPORT_SUCCESS, null);
}
return null;
});

通过以上伪代码可以做到 Excel 上传校验解析以及与文件服务器和主站的数据交互。

在给同事使用的过程中会偶发文件找不到的问题,经过定位后发现是异步开启后主线程处理完毕关闭 MultipartFile的 IO 流,使得上传的临时文件删除的问题。解决方案很直接,将上传的 multipartFile 转换成一个本地临时文件 flie,用这个临时文件在异步线程中解析,避免文件丢失,在使用完文件之后再主动删除。

/**
* @param mFile 上传文件
* @param identifier PTM2.0文件列表查询参数:唯一标识
* @Author WangZY
* @Date 2021/12/14 14:25
* @Description 导入文件--文件下载链接10天有效期
* @return 文件列表ID
**/
public ExcelUploadResDTO commonImportExcel(MultipartFile mFile, String identifier) {
if (mFile.isEmpty()) {
throw new PtmException("上传excel文件不能为空");
} else {
String fileName = mFile.getOriginalFilename();
if (StringUtils.isEmpty(fileName)) {
throw new PtmException("excel名称不能为空");
} else {
if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) {
throw new PtmException("excel格式不正确");
} else {
//组装Token和文件服务器权限信息发送请求
String token = getToken(true, 10);
if (mFile.getSize() <= 0) {
throw new PtmException("上传文件为空");
} else {
// 先判断文件夹是否存在,避免不存在时报错
String fileDir = commonProperties.getFileDir();
File filePathExist = new File(fileDir);
if (!filePathExist.exists()) {
boolean mkdir = filePathExist.mkdirs();
if (!mkdir) {
return null;
}
}
//将multipartFile转换为临时文件file,避免异步时子线程找不到文件实例
File file = new File(fileDir + mFile.getOriginalFilename());
try (BufferedInputStream bis = new BufferedInputStream(mFile.getInputStream());
BufferedOutputStream bos =
new BufferedOutputStream(Files.newOutputStream(file.toPath()))) {
int bytesRead = 0;
byte[] buffer = new byte[8192];
while ((bytesRead = bis.read(buffer, 0, 8192)) != -1) {
bos.write(buffer, 0, bytesRead);
}
} catch (Exception e) {
log.error("multipartFileToFile失败", e);
}
String fileId = ExternalApi.uploadFileServer(token, file, splicingFileServerUrl("upload"));
//组装文件信息,在PTM文件列表创建一条处理中的记录
long ptmFileId = createPtmFile(fileId, fileName, identifier, true);
return new ExcelUploadResDTO(ptmFileId, file);
}
}
}
}
}
/**
* @param file 文件
* @param clazz 类
* @Author WangZY
* @Date 2020/12/12 14:24
* @Description 读文件-文件使用后删除
**/
public List<?> readFile(File file, Class<?> clazz, boolean headCheck) {
ExcelListener excelListener = new ExcelListener();
EasyExcel.read(file, clazz, excelListener).sheet().doRead();
ExcelAnalyzeResDTO excelData = excelListener.getExcelData();
if (headCheck) {
checkHeadRight(clazz, excelData);
}
String dateError = excelData.getDateError();
log.info("完成文件解析,删除文件名={},临时文件结果={}", file.getName(), file.delete());
if (!StringUtils.isEmpty(dateError)) {
throw new PtmException(dateError);
} else {
return excelData.getExcelDataList();
}
}

这个 Excel 业务模块目前已经使用了两年时间,前后更新了十余次吧,非常稳定的一个组件。

文章目录
  1. 1. 前言
  2. 2. 案例一
  3. 3. 案例二
  4. 4. 案例三
  5. 5. 案例四
  6. 6. 案例五
  7. 7. 案例六
  8. 8. 案例七
  9. 9. 案例八