【SpringBoot|Java】玩具意义的另类@Async实现方式(上)

SpringBoot中@Async不使用AOP的玩具意义的实现方式 上

前言

总所周知 SpringBoot可以在方法上标注注解@Async 这样这个方法将会异步执行 在之前学习SpringBoot时 发现很多功能(事务,缓存,事件监听等)都是AOP实现 what can i say? 总是AOP 多没意思 有没有不使用AOP进行实现的方式呢

准备

首先要知道的是 对于这些使用AOP便利功能 都是在SpringBoot在启动时进行根据代理类模板创建代理类增强的 也能理解 如果到用的时候才创建 很有可能太慢了 那么我们也可以在这里入手 既然它是在启动过程中创建 我们能不能也在启动过程中进行一些适当的操作 来让这些被标注了特殊注解的方法被注意到呢 欸 我们可以想到的是 在一个Java程序运行时 会有字节码的编译 如果我们能在这里动点手脚 会不会有些希望呢?

既然是要和字节码交手 光靠我们自己能赢吗? 一定会输的 所以我们还是要借助工具 就是我们的ASM框架 可以在字节码生成之后进行查看,修改,添加的,无论是编译后立即进行,还是在类加载到JVM时动态进行 这也算一种启动过程中的手脚吧 通过ASM获取类的字节码信息 方法和其注解信息也能获取 这样不就能知道哪些类的哪些方法需要进行异步处理了吗
而且ASM获取信息 相较于反射而言 不会进行安全检查和再一次解析类信息 如果在一个项目中异步任务较多 会带来一些性能提升 至于安全性 我们保证只读取 不写入即可

再之后 我们需要对异步任务进行一些分级 如单纯的耗时长 但是不需要知道结果的 或者是 需要知道返回值结果的或者是定时任务 还有 需要动态调用的
其中 需要动态调用的方法 思考良久 还是只能采用动态代理增强的方式 否则无法应对动态的方法参数 和AOP创建代理增强类还是殊途同归了 有点烂尾的感觉…

前置知识

虽然是玩具项目 但是还是有一些干货的

  • 线程池(定时线程池 固定数量线程池等一些基本的线程池实现)
  • 反射
  • 自定义注解
  • 部分设计模式的理解(如观察者模式)
  • cglib动态代理
  • maven
    ASM的使用倒是也有 但是是很基础的查看字节码 因此可以不用特别了解

设计

如之前所述 我们将任务分为了多种类型 为了识别这多种类别的任务 我们需要多种注解来保证任务定义的方便性 之后在启动时识别这些注解 并根据类型将其转换成Runnable和Callable 并放入各自的执行器中进行执行 这里的执行器可以是一个线程池 只不过是包装过的 这样 我们就可以大概完成一个加上自定义注解 就可以转换成任务执行的操作了 流程图如下
在这里插入图片描述

依赖

整个项目使用的Maven依赖如下

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.fuys</groupId>
    <artifactId>fuys-task-manger</artifactId>
    <packaging>jar</packaging>
    <version>1.1</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>

        <!-- 字节码操作库 用于设置容器和辅助判断类型 -->
        <dependency>
            <groupId>org.ow2.asm</groupId>
            <artifactId>asm</artifactId>
            <version>6.0</version>
        </dependency>
        <dependency>
            <groupId>org.ow2.asm</groupId>
            <artifactId>asm-util</artifactId>
            <version>6.0</version>
        </dependency>

        <!-- 解析cron表达式的工具类 -->
        <dependency>
            <groupId>com.cronutils</groupId>
            <artifactId>cron-utils</artifactId>
            <version>9.1.6</version>
        </dependency>

        <!--cglib实现无接口实现的动态代理-->
        <dependency>
            <groupId>cglib</groupId>
            <artifactId>cglib</artifactId>
            <version>3.2.6</version>
        </dependency>

        <!-- lombok 简化类Get/Set 和 日志打印 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.16.18</version>
        </dependency>
        <!-- 用于单元测试 -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
        <!-- 日志打印实现类 -->
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.3.3</version>
        </dependency>
        
    </dependencies>
    
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.6.1</version>
            </plugin>
        </plugins>
    </build>
</project>

为了日志功能可以正常使用 在resources文件夹下配置日志的logback.xml文件如下

<?xml version="1.0" encoding="UTF-8"?>
<!-- 这一部分可能会有些网址和xsd文件报错 是idea配置的问题 不需要管 可以正常用 -->
<configuration
        xmlns="http://ch.qos.logback/xml/ns/logback"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://ch.qos.logback/xml/ns/logback logback.xsd">
    <!-- 输出控制,格式控制-->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%date{HH:mm:ss} [%-5level] [%thread] %logger{17} - %m%n </pattern>
        </encoder>
    </appender>
    <root level="DEBUG">
        <appender-ref ref="STDOUT"/>
    </root>
</configuration>

编码

我们编码采用先局部 再整体的思路来讲解 因此 我会先给出不同组件的实现 最后通过上下文来组合

任务类型注解

首先我们先编写自定义注解 如下面所示
在这里插入图片描述
有人可能会问了 为什么只有三个注解 我们是四种任务啊 其实无论有无返回值 我们都可以存在运行时的参数 因此 hasRuntimeParam() 是注解的一部分 这样方便我们进行判断
三种注解的代码如下

 /**
 * @description: 存在返回值的任务
 * @date: 2024/5/20 15:47
 * @version: 1.0
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface BeResultTask {
    String hasRuntimeParam();
}
/**
 * @description: 用于标识用户的普通任务
 * @date: 2024/5/20 15:46
 * @version: 1.0
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NoResultTask {
    String hasRuntimeParam();
}
/*
 * @description: 定时注解
 * @date: 2024/5/19 15:56
 * @version: 1.0
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Scheduled {
    String cron();
    String expression() default "";
}

定义任务发现类

我们从下到上来捋一遍这个流程
和SpringBoot类似 我们也需要扫描当前目录下的全部包的全部类(@ConpomentScan注解就不实现了 不是我们本文的重点) 在文件系统中 全类名和文件路径名是有些不同的 为了方便使用 我们定义一个文件工具类来帮助我们进行这个操作

package utils;

import core.finder.ComponentScanner;

import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.List;

/**
 * @description: 文件工具类
 * @date: 2024/5/21 16:09
 * @version: 1.0
 */
public class FileUtil {

    public static String fromClassNameToFileName(String name) {
        return name.replace('.', '/');
    }

    public enum MODE {
        CONTEXT(1),
        FILE(2);//枚举用于不同的模式 可能是文件或是Java上下文 两者的处理方式不同
        public final int value;

        MODE(int value) {
            this.value = value;
        }
    }

    /**
     * @param path: 根据传入的路径 扫描平级路径的全部文件
     * @return List<Path> 找到的所有文件路径
     * @description 查找文件
     * @date: 2024/5/21 16:09
     */
    public static List<Path> packagesHandle(Path path) throws IOException {
        if(path==null){
            return null;
        }
        List<Path> classes = new ArrayList<>();
        Files.walkFileTree(path, new SimpleFileVisitor<Path>() {
            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                if (file.toString().endsWith(".class")) {
                    classes.add(file);
                }
                return FileVisitResult.CONTINUE;
            }
        });
        return classes;

    }

    /**
     * @param clazz: 传入的class
     * @return File 返回文件对象
     * @description 根据类获取对应的磁盘文件
     * @date: 2024/5/21 16:09
     */
    public static File getDiskPath(Class<?> clazz) {
        URL location = clazz.getProtectionDomain().getCodeSource().getLocation();
        try {
            return Paths.get(location.toURI()).normalize().toFile();
        } catch (URISyntaxException e) {
            e.printStackTrace();
        }
        return null;
    }
    
    /**
     * @param fileName: 文件名 
     * @param mode: 模式
     * @return String 转换后的名称
     * @description 转换文件名到类名 通过模式设定
     * @date: 2024/5/21 16:09
     */
    public static String fromFileNameToClassName(String fileName, int mode) {
        if (mode == MODE.CONTEXT.value) {
            if (fileName.startsWith("L")) {
                String substring = fileName.substring(1, fileName.length() - 1);
                return substring.replace('/', '.');
            } else {
                return null;
            }
        }
        return fileName.replace('/', '.');
    }
}

同时我们需要定义任务定义 来保存扫描到的任务的信息

任务定义

package task;

import lombok.Data;

import java.util.List;

/**
 * @description: 任务定义信息 一个任务往往是一个类中的方法 这里采用记录的方法
 * @date: 2024/5/21 15:10
 * @version: 1.0
 */
@Data
public class TaskDefinition {
    private String className;
    private String methodName;
    private List<Class<?>> parameterTypes;
    private int type;
    //一般只有标注了 @Scheduled的方法才会有
    private String cron;
    //设置此任务是否由代理进行 即使是没有参数的代理也可以兼容 不过不推荐
    private Boolean enableProxy;
}

有了任务定义之后 对于扫描的任务定义 我们需要一个类来保存它们

任务定义持有者

package core.holder;

import task.TaskDefinition;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @description: 任务定义持有者
 * @date: 2024/5/21 15:46
 * @version: 1.0
 */
public class TaskDefinitionHolder {
    //全部任务定义存储Map
    public static Map<String, List<TaskDefinition>> taskDefinitionMap=new ConcurrentHashMap<>();
    //依照类型来找的任务存储Map
    public static Map<Integer, Set<String>> typeTaskMapKey=new ConcurrentHashMap<>();
    //需要代理的任务存储Map 由上下文解析并放入此Map
    public static Map<String,TaskDefinition> proxyTaskMap=new HashMap<>();
    public static void putDefinition(TaskDefinition taskDefinition){
        String classNameKey = taskDefinition.getClassName();
        String methodName = taskDefinition.getMethodName();

        String key=classNameKey+"."+methodName;
        if(taskDefinitionMap.containsKey(classNameKey)){
            taskDefinitionMap.get(classNameKey).add(taskDefinition);
        }else {
            List<TaskDefinition> list=new ArrayList<>();
            list.add(taskDefinition);
            taskDefinitionMap.put(classNameKey,list);
        }
        if(!typeTaskMapKey.containsKey(taskDefinition.getType())){
            Set<String> objects = new HashSet<>();
            objects.add(key);
            typeTaskMapKey.put(taskDefinition.getType(),objects);
        }else{
            typeTaskMapKey.get(taskDefinition.getType()).add(key);
        }
    }

    /**
     * @param key:类名
     * @return List<TaskDefinition>
     * @author WangJing
     * @description 根据类名查询任务
     * @date 2024/5/21 16:50
     */
    public static List<TaskDefinition> getDefinition(String key){
        List<TaskDefinition> list = taskDefinitionMap.get(key);
        return list;
    }
}

现在 我们需要完成使用ASM扫描的并记录需要处理的方法 为了能够自定义类的扫描逻辑 我们需要
继承ClassVistor 并重写visit和visitMethod方法 去观察 这个类 并做出相应的处理 当扫描到方法的时候 我们需要返回一个自定义的MethodVistor 原因同上

类观察者

package core.finder;

import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

/**
 * @description: 任务类观察者
 * @date: 2024/5/21 15:27
 * @version: 1.0
 */
public class TaskClassVisitor extends ClassVisitor {
    private String className;

    public TaskClassVisitor() {
        super(Opcodes.ASM6);
    }

    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        super.visit(version, access, name, signature, superName, interfaces);
        this.className=name;
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        return new TaskFinder(super.visitMethod(access, name, desc, signature, exceptions),className,name);
    }
}
方法观察者

package core.finder;

import annotation.task.BeResultTask;
import annotation.task.NoResultTask;
import annotation.task.Scheduled;
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import task.TaskDefinition;
import utils.FileUtil;
/**
 * @description: 利用Asm获取每个带有特殊注解的任务
 * @date: 2024/5/21 15:04
 * @version: 1.0
 */
public class TaskFinder extends MethodVisitor {
    //定义需要发现的注解
    private final String[] taskAnnotationDesc= {Type.getDescriptor(NoResultTask.class)
            ,Type.getDescriptor(BeResultTask.class),Type.getDescriptor(Scheduled.class)};

    private String className;
    private String methodName;
    public TaskFinder(MethodVisitor mv, String className,String methodName) {
        super(Opcodes.ASM6,mv);
        this.className=className;
        this.methodName=methodName;
    }

    @Override
    public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
        //每个方法最多只能是一个任务 多余的注解将被无视
        for (int i = 0; i < taskAnnotationDesc.length; i++) {
            if(taskAnnotationDesc[i].equals(desc)){
                TaskDefinition taskDefinition=new TaskDefinition();
                taskDefinition.setType(i);
                taskDefinition.setMethodName(methodName);
                String trueName = FileUtil.fromFileNameToClassName(className, FileUtil.MODE.FILE.value);
                taskDefinition.setClassName(trueName);
                    //这里进行定时任务的cron的获取
                return new TaskAnnotationVisitor(taskDefinition);
            }
        }
        return super.visitAnnotation(desc, visible);
    }
}

注解观察者

package core.finder;


import core.holder.TaskDefinitionHolder;
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.Opcodes;
import task.TaskDefinition;

/**
 * @description: 定时任务获取其中的注解值
 * @date: 2024/5/22 10:23
 * @version: 1.0
 */
public class TaskAnnotationVisitor extends AnnotationVisitor {
    private TaskDefinition taskDefinition;
    public TaskAnnotationVisitor(TaskDefinition taskDefinition) {
        super(Opcodes.ASM6);
        this.taskDefinition=taskDefinition;
    }

    @Override
    public void visit(String s, Object o) {
        //这里为什么不判空呢 首先 如果一个注解的属性 没有默认值 你不传值的时候 会报错的
        if("cron".equals(s)){
            String value = o.toString();
            this.taskDefinition.setCron(value);
            TaskDefinitionHolder.putDefinition(taskDefinition);
        }else if("expression".equals(s)){
            String value = o.toString();
            this.taskDefinition.setCron(value);
        }else if("hasRuntimeParam".equals(s)){
            Boolean value=Boolean.valueOf(o.toString());
            this.taskDefinition.setEnableProxy(value);
            TaskDefinitionHolder.putDefinition(taskDefinition);
        }
        super.visit(s,o);
    }
}

好 到目前为止 我们就完成了扫描并记录指定路径下的类的特殊注解标记的方法了 不过处理这些方法定义转换成Runnale和Callable的一个Task类是上下文的事情 我们下一步开发处理Runnable和Callable的Task类的处理器

定义处理器

和上面说的一样 这里只负责处理任务 任务定义和任务类还是有区别的 这里给出任务类的定义

任务类

package task;

import lombok.Data;
import lombok.extern.slf4j.Slf4j;

import java.util.Objects;
import java.util.concurrent.Callable;

/**
 * @description: 任务类
 * @date: 2024/5/19 15:54
 * @version: 1.0
 */
@Slf4j
@Data
public class Task {
   private Object task;
   private String taskName;


   public Callable transferToCallable(){
      if(task instanceof Callable)
      return (Callable) task;
      else return null;
   }

   public Runnable transferToRunnable(){
      if(task instanceof Runnable)
      return (Runnable) task;
      else return null;
   }

   @Override
   public boolean equals(Object other){
      //实际上 并没有为Runnable设置这个检查的必要 因为如果要使用HashMap
      //进行存储的话 肯定是需要返回值的 而Runnable没有返回值
      //但是为了健壮性 姑且把它加上把
      if(other==null)return false;
      if(!(other instanceof Task))return false;
      if(other==this)return true;
      Task task=(Task) other;
      if(task.task instanceof Runnable){
         if(this.task instanceof Runnable){
            return task.task==this.task;
         }else return false;
      }
      if(this.task instanceof Callable)
      return Objects.equals(task.task,this.task);
      return false;
   }

   @Override
   public int hashCode(){
     int result=17;
     if(task instanceof Callable){
        result=31*result+task.hashCode();
     }else if(task instanceof Runnable){
        result=31*result+Objects.hash(task.toString());
     }else{
        //其实这里应该抛出异常 但是感觉怪怪的 姑且使用Objects计算哈希把
        result=result*31+Objects.hash(task);
     }
     return result;
   }
   //重写equals和hashCode是为了让Map生效 否则无法区分key的唯一性 可能导致意外的问题
}

然后 我们将任务分成普通的任务和定时任务 普通的任务是除定时任务以外的其他任务 它们分别有自己的定义 如果是普通任务的话 其实和Task类是一样的 只有定时任务有些特殊

细分任务

package task.type;

import task.Task;

/**
 * @description: 普通任务
 * @date: 2024/5/19 15:59
 * @version: 1.0
 */
public class CommonTask extends Task {

}

package task.type;

import lombok.Data;
import lombok.EqualsAndHashCode;
import task.Task;

/**
 * @description: 定时任务
 * @date: 2024/5/19 15:59
 * @version: 1.0
 */
@Data
@EqualsAndHashCode(callSuper = true)
public class ScheduledTask extends Task implements WithScheduled {
//记录执行间隔
    private long period;
}

现在我们来完成任务的处理器

普通任务执行器

package core.handler;

import common.Pair;
import common.factory.NamedThreadFactory;
import constants.type.CoreSizeConstants;
import constants.type.TaskTypeConstants;
import exception.handle.HandleException;
import exception.handle.HandlerException;
import exception.task.TaskTypeException;
import lombok.extern.slf4j.Slf4j;
import task.Task;

import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.*;
import java.util.function.Supplier;

/**
 * @description: 普通任务处理器
 * @date: 2024/5/19 16:03
 * @version: 1.0
 */
@Slf4j
public class CommonHandler{
    private Map<String,CompletableFuture<?>> resultMap;
    private ThreadPoolExecutor executorService;


    public CommonHandler(Integer size){
        if(size==null){
            size= CoreSizeConstants.CORE_TYPE_CPU;
        }else if(size==0)size=CoreSizeConstants.CORE_TYPE_CPU;
        this.executorService=new ThreadPoolExecutor(size,size*2,300L, TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(),new NamedThreadFactory("common-handler"));
    }

//用于让用户设置自己想要获取任务结果时使用的map 如果用户没有提供 上下文会分配
    public void setFutureMap(Map<String,CompletableFuture<?>> futureMap){
        this.resultMap=futureMap;
    }

    public void handleTasks(List<Task> commonTasks) {

        for (Task commonTask : commonTasks) {
            //首先验证每个任务的类型 确保是普通任务
            //这一步也可以在上下文中直接进行 看情况吧
            Runnable runnable = commonTask.transferToRunnable();
            if(runnable==null){
                log.error("CommonHandler无法处理非runnable的任务");
                continue;
            }
            Future<?> submit = executorService.submit(runnable);
        }
    }

    /**
     * @param callableTasks: 需要执行的任务列表
     * @return void
     * @description 处理存在返回值的任务
     * @date 2024/5/21 11:05
     */
    public void handleHasResultTasks(List<Task> callableTasks) {
        //这里集中处理所有存在返回值的任务
        for (Task callableTask : callableTasks) {
            //首先验证每个任务的类型 确保是普通任务
            //这一步也可以在上下文中直接进行 看情况吧
            Callable callable = callableTask.transferToCallable();
            if(callable==null){
                log.error("CommonHandler无法处理非callable的任务:任务详情为"+callableTask.getTask().toString());
                continue;
            }
            Supplier supplier=()->{
                try {
                    final Object call = callable.call();
                    return call;
                } catch (Exception e) {
                    throw new HandleException("带有返回值的任务执行失败");
                }
            };
            CompletableFuture future=CompletableFuture.supplyAsync(supplier,executorService);
            resultMap.put(callableTask.getTaskName(),future);
        }
    }
}

定时任务执行器

package core.handler;

import common.Pair;
import common.factory.NamedThreadFactory;
import constants.type.CoreSizeConstants;
import lombok.extern.slf4j.Slf4j;
import task.type.ScheduledTask;
import task.Task;
import utils.ScheduleExpressionUtil;

import java.util.Map;
import java.util.Set;
import java.util.concurrent.*;

/**
 * @description: 定时任务处理器
 * @date: 2024/5/19 16:03
 * @version: 1.0
 */
@Slf4j
public class ScheduledHandler{
    //定时任务采用定时线程池完成
    private ScheduledExecutorService executorService;
    
    public  ScheduledHandler(Integer size){
        if(size==null)size=CoreSizeConstants.CORE_TYPE_CPU;
        this.executorService=Executors.newScheduledThreadPool(size,new NamedThreadFactory("scheduled-handler"));
    }

    //todo 这里暂不支持执行带有返回值的定时任务 或许未来会?
    public void handleTasks(Map<Task, String> scheduledTasks) {
        //解析Cron表达式 计算每个任务的执行周期
        Set<Task> tasks = scheduledTasks.keySet();
        for (Task task : tasks) {
            long period = ScheduleExpressionUtil.parseCronToPeriod(scheduledTasks.get(task));
            Runnable runTask = task.transferToRunnable();
            if(runTask==null){
                log.error("无法执行非Runnable的定时任务 当前任务类型为:"+task.getClass().getName());
                continue;
            }
            ScheduledFuture<?> scheduledFuture = executorService.scheduleAtFixedRate(runTask, period, period, TimeUnit.MILLISECONDS);
        }
    }
}

在里面出现了四个新类型 Pair,NamedThreadFactory,CoreSizeConstants,ScheduleExpressionUtil
还有一些类型异常 其实这些异常都高度相似 定义这些异常的作用是为了在上层调用时根据异常的不同来做出不同的反应
我们将逐一介绍它们的作用
首先是Pair 它就是一个轻量级的键值对 用于存储一些数据

package common;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Objects;

/**
 * @description: 轻量键值对
 * @date: 2024/5/22 10:36
 * @version: 1.0
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Pair<L,R> {
    private L left;
    private R right;

    @Override
    public  boolean equals(Object o){
        if(this==o)return true;
        if(o==null||this.getClass()!=o.getClass())return false;
        Pair<?,?> pair=(Pair<?, ?>) o;
        return left.equals(pair.left)&& right.equals(pair.right);
    }

    @Override
    public int hashCode(){
        return Objects.hash(left,right);
    }
}

然后是NamedThreadFactory 和注释所言一样 为了给线程池命名 方便区分执行任务的线程

package common.factory;

import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @description: 线程命名工厂
 * @date: 2024/6/7 10:28
 * @version: 1.0
 */
public class NamedThreadFactory implements ThreadFactory {

    private String prefix;
    private AtomicInteger integer=new AtomicInteger(1);

    public NamedThreadFactory(String prefix){
        this.prefix=prefix;
    }
    @Override
    public Thread newThread(Runnable r) {
        Thread thread=new Thread(r,prefix+"-"+integer.getAndIncrement());
        thread.setDaemon(true);
        return thread;
    }
}

然后是CoreSizeConstants

package constants.type;

/**
 * @description: 线程池核心设置常量
 * @date: 2024/5/19 16:09
 * @version: 1.0
 */
public class CoreSizeConstants {
    //获取当前计算机的可用核心数
    private static final int CORE_NUMS = (Runtime.getRuntime().availableProcessors()+1)/2;
    //计算密集型(CPU使用多)分配的线程数
    public static final int CORE_TYPE_CPU = CORE_NUMS+1;
    //IO密集型(磁盘操作使用多)分配的线程数
    public static final int CORE_TYPE_MIXED = CORE_NUMS+CORE_NUMS/2;
    //混合型
    public static final int CORE_TYPE_IO=CORE_NUMS*2;
}

ScheduleExpressionUtil 是用于计算定时任务的执行间隔

package utils;

import com.cronutils.model.Cron;
import com.cronutils.model.CronType;
import com.cronutils.model.definition.CronDefinition;
import com.cronutils.model.definition.CronDefinitionBuilder;
import com.cronutils.model.time.ExecutionTime;
import com.cronutils.parser.CronParser;
import lombok.extern.slf4j.Slf4j;

import java.time.Duration;
import java.time.ZoneId;
import java.time.ZonedDateTime;

/**
 * @description: 定时任务表达式解析工具类
 * @date: 2024/5/20 17:19
 * @version: 1.0
 */
@Slf4j
public class ScheduleExpressionUtil {
    public static long parseCronToPeriod(String cron){
        //解析Cron表达式
        CronDefinition cronDefinition= CronDefinitionBuilder.instanceDefinitionFor(CronType.QUARTZ);
        CronParser parser=new CronParser(cronDefinition);
        Cron parsedCron=parser.parse(cron);
        //获取第一次执行时间
        ExecutionTime executionTime=ExecutionTime.forCron(parsedCron);
        ZonedDateTime now = ZonedDateTime.now();
        //计算下一次执行时间
        ZonedDateTime nextExecution = executionTime.nextExecution(now).orElseThrow(RuntimeException::new);
        //获取间隔时间
        ZonedDateTime nextNextExecution=executionTime.nextExecution(nextExecution.toLocalDateTime().atZone(ZoneId.systemDefault())).orElseThrow(RuntimeException::new);
        Duration periodDuration = Duration.between(nextExecution, nextNextExecution);
        return periodDuration.toMillis();
    }

}

各种类型异常的介绍 这个需要搭配之后开发的上下文来理解

package exception.handle;

import lombok.Data;

/**
 * @description: 处理任务时的异常
 * @date: 2024/5/19 17:15
 * @version: 1.0
 */
@Data
public class HandleException extends RuntimeException{
    private String msg;
    public HandleException(String message)
    {
        super(message);
        this.msg=message;
    }
}

package exception.handle;

/**
 * @description: 处理器本身的问题导致的异常
 * @date: 2024/5/19 17:15
 * @version: 1.0
 */
public class HandlerException extends RuntimeException{
    private String message;
    public HandlerException(String message)
    {
        this.message = message;
    }
}

package exception.task;

/**
 * @description: 任务类型转换异常 一般是不合法的操作
 * @date: 2024/5/19 17:39
 * @version: 1.0
 */
public class TaskTypeException extends RuntimeException{
    private String message;
    public TaskTypeException(String message)
    {
        super(message);
        this.message=message;
    }
}

到这里 我们就完成了对任务执行器的设置 最后就该轮到把这些积木组成一个建筑了 也就是上下文的开发

🤡

编辑文章到现在以及卡的打字都困难了

解释

由于下文的编码还很多 把markdown编辑器都整的卡的打不了字 因此拆分成上下 望读者海涵
如果复制代码后出现错误 请评论或私信 因为博客写的git版本是之前的 到现在有一些更改 因此是回退版本并组合的 带来的不便望原谅
下篇正在火速更新中…

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值