• 如何优雅的使用用AOP实现异步上传(荣耀典藏版)


    目录

    前言

    代码与实现

    结语


    前言

    相信很多系统里都有这一种场景:用户上传Excel,后端解析Excel生成相应的数据,校验数据并落库。这就引发了一个问题:如果Excel的行非常多,或者解析非常复杂,那么解析+校验的过程就非常耗时。

    如果接口是一个同步的接口,则非常容易出现接口超时,进而返回的校验错误信息也无法展示给前端,这就需要从功能上解决这个问题。一般来说都是启动一个子线程去做解析工作,主线程正常返回,由子线程记录上传状态+校验结果到数据库。同时提供一个查询页面用于实时查询上传的状态和校验信息。

    进一步的,如果我们每一个上传的任务都写一次线程池异步+日志记录的代码就显得非常冗余。同时,非业务代码也侵入了业务代码导致代码可读性下降。

    从通用性的角度上讲,这种业务场景非常适合模板方法的设计模式。即设计一个抽象类,定义上传的抽象方法,同时实现记录日志的方法,例如:

    1. //伪代码,省略了一些步骤
    2. @Slf4j
    3. public abstract class AbstractUploadService<T> {
    4. public static ThreadFactory commonThreadFactory = new ThreadFactoryBuilder().setNameFormat("-upload-pool-%d")
    5. .setPriority(Thread.NORM_PRIORITY).build();
    6. public static ExecutorService uploadExecuteService = new ThreadPoolExecutor(10, 20, 300L,
    7. TimeUnit.SECONDS, new LinkedBlockingQueue<>(1024), commonThreadFactory, new ThreadPoolExecutor.AbortPolicy());
    8. protected abstract String upload(List<T> data);
    9. protected void execute(String userName, List<T> data) {
    10. // 生成一个唯一编号
    11. String uuid = UUID.randomUUID().toString().replace("-", "");
    12. uploadExecuteService.submit(() -> {
    13. // 记录日志
    14. writeLogToDb(uuid, userName, updateTime, "导入中");
    15. // 一个字符串,用于记录upload的校验信息
    16. String errorLog = "";
    17. //执行上传
    18. try {
    19. errorLog = upload(data);
    20. writeSuccess(uuid, "导入中", updateTime);
    21. } catch (Exception e) {
    22. LOGGER.error("导入错误", e);
    23. //计入导入错误日志
    24. writeFailToDb(uuid, "导入失败", e.getMessage(), updateTime);
    25. }
    26. /**
    27. * 检查一下upload是不是返回了错误日志,如果有,需要注意记录
    28. *
    29. * 因为错误日志可能比较长,
    30. * 可以写入一个文件然后上传到公司的文件服务器,
    31. * 然后在查看结果的时候允许用户下载该文件,
    32. * 这里不展开只做示意
    33. */
    34. if (StringUtils.isNotEmpty(errorLog)) {
    35. writeFailToDb(uuid, "导入失败", errorLog, updateTime);
    36. }
    37. });
    38. }
    39. }

    如上文所示,模板方法的方式虽然能够极大地减少重复代码,但是仍有下面两个问题:

    • upload方法得限定死参数结构,一旦有变化,不是很容易更改参数类型or数量

    • 每个上传的service还是要继承一下这个抽象类,还是不够简便和优雅

    为解决上面两个问题,我也经常进行思考,结果在某次自定义事务提交or回滚的方法的时候得到了启发。这个上传的逻辑过程和事务提交的逻辑过程非常像,都是在实际操作前需要做初始化操作,然后在异常或者成功的时候做进一步操作。这种完全可以通过环装切面的方式实现,由此,我写了一个小轮子给团队使用。

    当然了,这个小轮子在本人所在的大团队内部使用的很好,但是不一定适合其他人,但是思路一样,大家可以扩展自己的功能

    多说无益,上代码!

    代码与实现

    1、首先定义一个日志实体

    1. public class FileUploadLog {
    2. private Integer id;
    3. // 唯一编码
    4. private String batchNo;
    5. // 上传到文件服务器的文件key
    6. private String key;
    7. // 错误日志文件名
    8. private String fileName;
    9. //上传状态
    10. private Integer status;
    11. //上传人
    12. private String createName;
    13. //上传类型
    14. private String uploadType;
    15. //结束时间
    16. private Date endTime;
    17. // 开始时间
    18. private Date startTime;
    19. }

    2、然后定义一个上传的类型枚举,用于记录是哪里操作的

    1. public enum UploadType {
    2. 未知(1,"未知"),
    3. 类型2(2,"类型2"),
    4. 类型1(3,"类型1");
    5. private int code;
    6. private String desc;
    7. private static Map<Integer, UploadType> map = new HashMap<>();
    8. static {
    9. for (UploadType value : UploadType.values()) {
    10. map.put(value.code, value);
    11. }
    12. }
    13. UploadType(int code, String desc) {
    14. this.code = code;
    15. this.desc = desc;
    16. }
    17. public int getCode() {
    18. return code;
    19. }
    20. public String getDesc() {
    21. return desc;
    22. }
    23. public static UploadType getByCode(Integer code) {
    24. return map.get(code);
    25. }
    26. }

    3、定义一个注解,用于标识切点

    1. @Retention(RetentionPolicy.RUNTIME)
    2. @Target({ElementType.METHOD})
    3. public @interface Upload {
    4. // 记录上传类型
    5. UploadType type() default UploadType.未知;
    6. }

    4、编写切面

    1. @Component
    2. @Aspect
    3. @Slf4j
    4. public class UploadAspect {
    5. public static ThreadFactory commonThreadFactory = new ThreadFactoryBuilder().setNameFormat("upload-pool-%d")
    6. .setPriority(Thread.NORM_PRIORITY).build();
    7. public static ExecutorService uploadExecuteService = new ThreadPoolExecutor(10, 20, 300L,
    8. TimeUnit.SECONDS, new LinkedBlockingQueue<>(1024), commonThreadFactory, new ThreadPoolExecutor.AbortPolicy());
    9. @Pointcut("@annotation(com.aaa.bbb.Upload)")
    10. public void uploadPoint() {}
    11. @Around(value = "uploadPoint()")
    12. public Object uploadControl(ProceedingJoinPoint pjp) {
    13. // 获取方法上的注解,进而获取uploadType
    14. MethodSignature signature = (MethodSignature)pjp.getSignature();
    15. Upload annotation = signature.getMethod().getAnnotation(Upload.class);
    16. UploadType type = annotation == null ? UploadType.未知 : annotation.type();
    17. // 获取batchNo
    18. String batchNo = UUID.randomUUID().toString().replace("-", "");
    19. // 初始化一条上传的日志,记录开始时间
    20. writeLogToDB(batchNo, type, new Date)
    21. // 线程池启动异步线程,开始执行上传的逻辑,pjp.proceed()就是你实现的上传功能
    22. uploadExecuteService.submit(() -> {
    23. try {
    24. String errorMessage = pjp.proceed();
    25. // 没有异常直接成功
    26. if (StringUtils.isEmpty(errorMessage)) {
    27. // 成功,写入数据库,具体不展开了
    28. writeSuccessToDB(batchNo);
    29. } else {
    30. // 失败,因为返回了校验信息
    31. fail(errorMessage, batchNo);
    32. }
    33. } catch (Throwable e) {
    34. LOGGER.error("导入失败:", e);
    35. // 失败,抛了异常,需要记录
    36. fail(e.toString(), batchNo);
    37. }
    38. });
    39. return new Object();
    40. }
    41. private void fail(String message, String batchNo) {
    42. // 生成上传错误日志文件的文件key
    43. String s3Key = UUID.randomUUID().toString().replace("-", "");
    44. // 生成文件名称
    45. String fileName = "错误日志_" +
    46. DateUtil.dateToString(new Date(), "yyyy年MM月dd日HH时mm分ss秒") + ExportConstant.txtSuffix;
    47. String filePath = "/home/xxx/xxx/" + fileName;
    48. // 生成一个文件,写入错误数据
    49. File file = new File(filePath);
    50. OutputStream outputStream = null;
    51. try {
    52. outputStream = new FileOutputStream(file);
    53. outputStream.write(message.getBytes());
    54. } catch (Exception e) {
    55. LOGGER.error("写入文件错误", e);
    56. } finally {
    57. try {
    58. if (outputStream != null)
    59. outputStream.close();
    60. } catch (Exception e) {
    61. LOGGER.error("关闭错误", e);
    62. }
    63. }
    64. // 上传错误日志文件到文件服务器,我们用的是s3
    65. upFileToS3(file, s3Key);
    66. // 记录上传失败,同时记录错误日志文件地址到数据库,方便用户查看错误信息
    67. writeFailToDB(batchNo, s3Key, fileName);
    68. // 删除文件,防止硬盘爆炸
    69. deleteFile(file)
    70. }
    71. }

    至此整个异步上传功能就完成了,是不是很简单?(笑)

    那么怎么使用呢?更简单,只需要在service层加入注解即可,顶多就是把错误信息return出去。

    1. @Upload(type = UploadType.类型1)
    2. public String upload(List<ClassOne> items) {
    3. if (items == null || items.size() == 0) {
    4. return;
    5. }
    6. //校验
    7. String error = uploadCheck(items);
    8. if (StringUtils.isNotEmpty) {
    9. return error;
    10. }
    11. //删除旧的
    12. deleteAll();
    13. //插入新的
    14. batchInsert(items);
    15. }

    结语

    写了个小轮子提升团队整体开发效率感觉真不错。程序员的最高品质就是解放双手(偷懒?),然后成功的用自己写的代码把自己干毕业。。。。。。

    来源:https://blog.csdn.net/weixin_48321993/article/details/125877904
  • 相关阅读:
    zookeeper 介绍
    多线程、并发及线程的基础问题
    RabbitMQ
    关于JAVA IO流的学习
    SQL 的基本常识
    What is Bt?
    Python turtle库的学习笔记
    字符串简单模式匹配算法与IndexOf方法比较
    谈如何选书
    使用JavaScriptSerializer进行序列化日期类型应该注意的问题
  • 原文地址:https://www.cnblogs.com/konglxblog/p/16739863.html
Copyright © 2020-2023  润新知