Implémentation d'une petite roue - utilisation d'AOP pour réaliser un téléchargement asynchrone

avant-propos

Je pense qu'il existe un tel scénario dans de nombreux systèmes : l'utilisateur télécharge Excel, le back-end analyse Excel pour générer les données correspondantes, et les données sont vérifiées et stockées. Cela pose un problème : si l'Excel a beaucoup de lignes, ou si l'analyse est très compliquée, le processus d'analyse + vérification prend beaucoup de temps. Si l'interface est une interface synchrone, il est très facile pour l'interface d'expirer et les informations d'erreur de vérification renvoyées ne peuvent pas être affichées sur le frontal.Ce problème doit être résolu de manière fonctionnelle. De manière générale, un sous-thread est démarré pour effectuer le travail d'analyse, le thread principal revient normalement et le sous-thread enregistre l'état de téléchargement + le résultat de la vérification dans la base de données. Dans le même temps, une page de requête est fournie pour interroger l'état et les informations de vérification du téléchargement en temps réel.

Dessin 1.jpg

De plus, si nous écrivons le code du pool de threads asynchrone + journalisation une fois pour chaque tâche de téléchargement, il sera très redondant. Dans le même temps, le code non métier envahit également le code métier, ce qui réduit la lisibilité du code. Du point de vue de la généralité, ce type de scénario métier est très adapté au design pattern de la méthode template. C'est-à-dire concevoir une classe abstraite, définir la méthode abstraite de téléchargement et implémenter la méthode de journalisation, par exemple :

//伪代码,省略了一些步骤
@Slf4j
public abstract class AbstractUploadService<T> {
   public static ThreadFactory commonThreadFactory = new ThreadFactoryBuilder().setNameFormat("-upload-pool-%d")
      .setPriority(Thread.NORM_PRIORITY).build();
   public static ExecutorService uploadExecuteService = new ThreadPoolExecutor(10, 20, 300L,
      TimeUnit.SECONDS, new LinkedBlockingQueue<>(1024), commonThreadFactory, new ThreadPoolExecutor.AbortPolicy());

   protected abstract String upload(List<T> data);

   protected void execute(String userName, List<T> data) {
      // 生成一个唯一编号
      String uuid = UUID.randomUUID().toString().replace("-", "");
      uploadExecuteService.submit(() -> {
         // 记录日志
         writeLogToDb(uuid, userName, updateTime, "导入中");
         // 一个字符串,用于记录upload的校验信息
         String errorLog = "";
         //执行上传
         try {
            errorLog = upload(data);
            writeSuccess(uuid, "导入中", updateTime);
         } catch (Exception e) {
            LOGGER.error("导入错误", e);
            //计入导入错误日志
            writeFailToDb(uuid, "导入失败", e.getMessage(), updateTime);
         }
         /**
          * 检查一下upload是不是返回了错误日志,如果有,需要注意记录
          *
          * 因为错误日志可能比较长,
          * 可以写入一个文件然后上传到公司的文件服务器,
          * 然后在查看结果的时候允许用户下载该文件,
          * 这里不展开只做示意
          */
         if (StringUtils.isNotEmpty(errorLog)) {
            writeFailToDb(uuid, "导入失败", errorLog, updateTime);
         }

      });
   }
}
复制代码

Comme indiqué ci-dessus, bien que la méthode du modèle puisse réduire considérablement le code répétitif, elle présente toujours les deux problèmes suivants :

  • La méthode de téléchargement doit limiter la structure des paramètres morts. Une fois qu'il y a un changement, il n'est pas facile de changer le type ou la quantité du paramètre
  • Chaque service téléchargé doit encore hériter de cette classe abstraite, ce qui n'est pas assez simple et élégant.

Afin de résoudre les deux problèmes ci-dessus, j'y pense souvent et, par conséquent, je me suis inspiré d'une méthode de validation ou de restauration de transaction personnalisée. Le processus logique de téléchargement est très similaire au processus logique de soumission de transaction, qui doivent tous deux être initialisés avant l'opération proprement dite, puis d'autres opérations sont effectuées en cas d'exception ou de réussite. Cela peut être réalisé complètement en faisant sonner les tranches, j'ai donc écrit une petite roue à utiliser par l'équipe. (Bien sûr, cette petite roue est très bien utilisée dans ma grande équipe, mais elle peut ne pas convenir à d'autres, mais l'idée est la même, chacun peut étendre ses propres fonctions)

Inutile d'en dire plus, codez !

code et implémentation

Définissez d'abord une entité de journal

public class FileUploadLog {
   private Integer id;
    // 唯一编码
    private String batchNo;
    // 上传到文件服务器的文件key
    private String key;
    // 错误日志文件名
    private String fileName;
    //上传状态
    private Integer status;
    //上传人
    private String createName;
    //上传类型
    private String uploadType;
    //结束时间
    private Date endTime;
    // 开始时间
    private Date startTime;
}
复制代码

Définissez ensuite une énumération de type de téléchargement pour enregistrer où l'opération est

public enum UploadType {
   未知(1,"未知"),
   类型2(2,"类型2"),
   类型1(3,"类型1");
   
   private int code;
   private String desc;
   private static Map<Integer, UploadType> map = new HashMap<>();
   static {
      for (UploadType value : UploadType.values()) {
         map.put(value.code, value);
      }
   }

   UploadType(int code, String desc) {
      this.code = code;
      this.desc = desc;
   }

   public int getCode() {
      return code;
   }

   public String getDesc() {
      return desc;
   }

   public static UploadType getByCode(Integer code) {
      return map.get(code);
   }
}
复制代码

Enfin, définissez une annotation pour identifier le point de coupe

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Upload {
   // 记录上传类型
   UploadType type() default UploadType.未知;
}
复制代码

Ensuite, écrivez la tranche

@Component
@Aspect
@Slf4j
public class UploadAspect {
   public static ThreadFactory commonThreadFactory = new ThreadFactoryBuilder().setNameFormat("upload-pool-%d")
      .setPriority(Thread.NORM_PRIORITY).build();
   public static ExecutorService uploadExecuteService = new ThreadPoolExecutor(10, 20, 300L,
      TimeUnit.SECONDS, new LinkedBlockingQueue<>(1024), commonThreadFactory, new ThreadPoolExecutor.AbortPolicy());


   @Pointcut("@annotation(com.aaa.bbb.Upload)")
   public void uploadPoint() {}

   @Around(value = "uploadPoint()")
   public Object uploadControl(ProceedingJoinPoint pjp) {
       // 获取方法上的注解,进而获取uploadType
      MethodSignature signature = (MethodSignature)pjp.getSignature();
      Upload annotation = signature.getMethod().getAnnotation(Upload.class);
      UploadType type = annotation == null ? UploadType.未知 : annotation.type();
      // 获取batchNo
      String batchNo = UUID.randomUUID().toString().replace("-", "");
      // 初始化一条上传的日志,记录开始时间
      writeLogToDB(batchNo, type, new Date)
      // 线程池启动异步线程,开始执行上传的逻辑,pjp.proceed()就是你实现的上传功能
      uploadExecuteService.submit(() -> {
         try {
            String errorMessage = pjp.proceed();
            // 没有异常直接成功
            if (StringUtils.isEmpty(errorMessage)) {
                // 成功,写入数据库,具体不展开了
                writeSuccessToDB(batchNo);
            } else {
                // 失败,因为返回了校验信息
                fail(errorMessage, batchNo);
            }
         } catch (Throwable e) {
            LOGGER.error("导入失败:", e);
            // 失败,抛了异常,需要记录
            fail(e.toString(), batchNo);
         }
      });
      return new Object();
   }

   private void fail(String message, String batchNo) {
       // 生成上传错误日志文件的文件key
      String s3Key = UUID.randomUUID().toString().replace("-", "");
      // 生成文件名称
      String fileName = "错误日志_" +
         DateUtil.dateToString(new Date(), "yyyy年MM月dd日HH时mm分ss秒") + ExportConstant.txtSuffix;
      String filePath = "/home/xxx/xxx/" + fileName;
      // 生成一个文件,写入错误数据
      File file = new File(filePath);
      OutputStream outputStream = null;
      try {
         outputStream = new FileOutputStream(file);
         outputStream.write(message.getBytes());

      } catch (Exception e) {
         LOGGER.error("写入文件错误", e);
      } finally {
         try {
            if (outputStream != null)
               outputStream.close();
         } catch (Exception e) {
            LOGGER.error("关闭错误", e);
         }
      }
      // 上传错误日志文件到文件服务器,我们用的是s3
      upFileToS3(file, s3Key);
      // 记录上传失败,同时记录错误日志文件地址到数据库,方便用户查看错误信息
      writeFailToDB(batchNo, s3Key, fileName);
      // 删除文件,防止硬盘爆炸
      deleteFile(file)
   }

}
复制代码

À ce stade, toute la fonction de téléchargement asynchrone est terminée. Est-ce très simple ? (rire)

Alors comment l'utiliser ? C'est plus simple, il suffit d'ajouter des annotations à la couche de service et de renvoyer au maximum les informations d'erreur.

@Upload(type = UploadType.类型1)
public String upload(List<ClassOne> items)  {
   if (items == null || items.size() == 0) {
      return;
   }
   //校验
   String error = uploadCheck(items);
   if (StringUtils.isNotEmpty) {
       return error;
   }
   //删除旧的
   deleteAll();
   //插入新的
   batchInsert(items);
}
复制代码

Épilogue

Cela fait vraiment du bien d'écrire une petite roue pour améliorer l'efficacité globale du développement de l'équipe. La plus haute qualité d'un programmeur est de libérer ses mains (paresseux ?), puis de réussir son diplôme avec le code qu'il a écrit. . . . . .

a2988928-646c-4a41-827b-30cc41dd0ae5.gif

WeChat image_20220527165700.jpg

Je suppose que tu aimes

Origine juejin.im/post/7102343528525037576
conseillé
Classement