实战:Java中一个注解实现分批处理+合并结果

实战:针对实际项目中遇到的场景进行开发

一、目的和思路

目的: 用一个注解实现分批处理、并发执行,然后合并结果返回。一个注解指的是自定义注解,然后利用 AOP 实现。

主要思路: 通过 AOP 的 @Around 注解拦截所有的 自定义注解 标记的方法,并对其参数进行“自动分批+并发”处理。

这种设计在处理大批量集合型参数(如 List)的接口时非常高效,可以自动将任务拆分为多个小任务并并发执行,最后聚合结果返回。

二、应用场景

应用场景: 某些方法中大量集合参数,在达到一定阈值时进行分批并发执行,提高吞吐量并最终聚合结果返回。

三、代码实现前需知道

在代码实现之前,需要知道几个知识点,然后后面的代码才容易看得懂!

1. joinPoint获取目标方法参数

就是在 AOP 中,通过连接点获取目标方法的参数时,需要注意下,先看下代码:

MethodSignature methodSignature = joinPoint.getSignature();
Method method = methodSignature.getMethod();

// 获取参数:
Object[] args = joinPoint.getArgs();

// 获取参数:
Parameter[] parameter = method.getParameters();

这两个是 Java 反射机制中常见的 API,分别用于

  • joinPoint.getArgs():获取方法调用时的 实参值
  • method.getParameters():方法定义时的 形参信息(包括:名称、类型、标记形参的注解)

举例:

有个目标方法,有3个参数(第一个参数有自定义注解)

public void processData(@MyAnno String name, int age, List<String> dataList) {
    System.out.println("执行方法:" + name + ", " + age + ", " + dataList);
}

切面类:

@Around("....")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
    MethodSignature signature = (MethodSignature) joinPoint.getSignature();
    Method method = signature.getMethod();

    // 1. 获取运行时的参数值
    Object[] args = joinPoint.getArgs(); // 实际值,例如 ["Tom", 30, ["a", "b", "c"]]

    // 2. 获取形参信息(包含注解、类型等)
    Parameter[] parameters = method.getParameters(); // Parameter对象

    System.out.println("方法参数列表:");
    for (int i = 0; i < parameters.length; i++) {
        Parameter param = parameters[i];
        Object argValue = args[i];

        System.out.println("第" + i + "个参数:");
        System.out.println("  参数名: " + param.getName());
        System.out.println("  参数类型: " + param.getType().getSimpleName());
        System.out.println("  运行时值: " + argValue);
        System.out.println("  是否有@MyAnno注解: " + param.isAnnotationPresent(MyAnno.class));
    }

    return joinPoint.proceed();
}

控制台输出示例(假设传入 ("Tom", 30, Arrays.asList("a", "b", "c"))):

方法参数列表:
第0个参数:
  参数名: name
  参数类型: String
  运行时值: Tom
  是否有@MyAnno注解: true

第1个参数:
  参数名: age
  参数类型: int
  运行时值: 30
  是否有@MyAnno注解: false

第2个参数:
  参数名: dataList
  参数类型: List
  运行时值: [a, b, c]
  是否有@MyAnno注解: false
2. joinPoint替换目标方法参数
调用方式说明场景
proceed()调用原始方法,参数不变只读、不变更参数的切面
proceed(Object[] args)调用原始方法,但使用新的参数数组需要修改方法参数(如拆分、拦截器等)
3. 关于Collection中的add和addAll
方法名参数类型功能描述
add(E e)单个元素(泛型对象)向集合中添加一个元素
addAll(Collection<? extends E> c)一个集合向集合中添加另一个集合中的所有元素

举个例子:

List<String> list1 = new ArrayList<>();
list1.add("A");

List<String> list2 = Arrays.asList("B", "C");

使用 add:list1.add(list2),则内容为:["A",["B","C"]],把整个 list2 当作一个对象添加进来了 ,而不是把里面的 “B”、“C” 分别加入。

使用 addAll:list1.addAll(list2),则内容为:["A","B","C"],把 list2 的所有元素摊开加入到 list1 中


四、代码实现

1、先自定义注解:

/**
 * 注解:标记需要拆分的参数
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER})
public @interface NeedSplitParam {

}
/**
 * 主注解:标记需要处理的方法
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.CONSTRUCTOR, ElementType.METHOD})
public @interface SplitWork {

    // 返回值处理,默认策略类为HandleReturnImpl
    Class<? extends HandleReturn> handlerReturnClass() default HandleReturnImpl.class;

    // 超过多少开始拆分
    int splitLimit() default 1000;

    // 拆分后每组多少
    int splitGroupNum() default 100;
}

2、线程池配置类:

/**
 * 线程池配置
 */
public class ThreadPoolConfig {
    private final int maxWorkers;

    public ThreadPoolConfig(int maxWorkers) {
        this.maxWorkers = maxWorkers;
    }

    public ExecutorService createThreadPool() {
        return Executors.newFixedThreadPool(maxWorkers);
    }
}

3、控制类:

/**
 * 实战:用一个注解实现分批处理、合并结果
 */
@RestController
public class UserController {

    @PostMapping("/listDeviceDetail")
    @SplitWork(splitLimit = 20, splitGroupNum = 10)
    public List<Long> listDeviceDetail(@RequestBody @NeedSplitParam List<Long> deviceIds) {
        List<Long> lists = new ArrayList<>();
        lists.addAll(deviceIds);
        return lists;
    }
}

4、返回数据处理类:

/**
 * 把多个批次的返回结果进行统一合并 并 返回
 */
public interface HandleReturn {

    /**
     * 处理返回结果的方法
     * @param results 拆分后多次请求结果
     * @return 处理后的返回结果
     */
    Object handleReturn(List results);
}
/**
 * 分批执行后,用来合并返回值的默认策略实现类
 */
public class HandleReturnImpl implements HandleReturn {

    // List:每一个元素是你之前通过多线程 CompletableFuture.get() 得到的子任务返回值;
    @Override
    public Object handleReturn(List results) {
        // 如果没有任何结果直接返回null
        if (results == null)
            return null;
        // 如果只有一个批次的结果(或者没有拆分),就直接返回这个结果
        if (results.size() <= 1)
            return results.get(0);
        /* 拿第一个结果做基础合并对象:
            1.假设所有批次的返回值类型都是List;
            2.取第一个作为合并目标容器,后面的批次结果都将追加到这个集合里
          注意:所有结果都是List类型,否则会抛出类型转换异常
         */
        List first = (List) results.get(0);
        for (int i = 1; i < results.size(); i++) {
            first.addAll((List) results.get(i));
        }
        // 是所有分批执行结果的整合
        return first;
    }
}

5、主要切面类:

@Aspect
@Component
@Slf4j
public class SplitWorkerAspect {

    // 1.识别并标注@SplitWork注解
    @Pointcut("@annotation(com.demo.anno.SplitWork)")
    public void needSplit() {
    }

    /**
     * 对某些特定方法的入参进行拆分,分批处理并并发执行,然后统一处理结果返回
     */
    @Around("needSplit()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        // 通过连接点获取到方法的签名
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        // 通过方法签名获取到方法
        Method method = methodSignature.getMethod();
        // 拿注解中的一些信息
        SplitWork splitWorkAnnotation = method.getAnnotation(SplitWork.class);
        int splitLimit = splitWorkAnnotation.splitLimit();
        int splitGroupNum = splitWorkAnnotation.splitGroupNum();

        // 拿参数:目标方法的实参
        Object[] args = joinPoint.getArgs();

        // 判断参数是否可拆
        if (args == null || args.length == 0 || splitLimit <= splitGroupNum) {
            // 没有参数、splitLimit 小于或等于 splitGroupNum,不合理; → 都不拆分
            return joinPoint.proceed();
        }

        // 2.找出需要拆分的参数位置
        int needSplitParamIndex = -1;
        // 拿参数:目标方法的形参信息(包括参数名称、类型、注解)
        Parameter[] parameters = method.getParameters();
        // 注意:这里只在方法的参数列表中查找第一个被 @NeedSplitParam 注解标记的参数,并记录其索引。
        // 如果目标方法有多个参数被 @NeedSplitParam 注解标记,这段代码只会记录第一个找到的参数的索引,然后立即 break 循环。
        for (int i = 0; i < parameters.length; i++) {
            // i=0就是第一个形参信息,i=1就是第二个形参信息
            Parameter parameter = parameters[i];
            // 判断参数中是否有被@NeedSplitParam注解标识
            NeedSplitParam needSplitParamAnno = parameter.getAnnotation(NeedSplitParam.class);
            if (needSplitParamAnno != null) {
                needSplitParamIndex = i;
                break;
            }
        }
        if (needSplitParamIndex == -1) {
            return joinPoint.proceed();
        }

        // 3.类型合法性 与 最小拆分长度判断
        // 非集合或数组,不支持拆分
        Object needSplitParam = args[needSplitParamIndex];
        if (!(needSplitParam instanceof Object[]) && !(needSplitParam instanceof List) && !(needSplitParam instanceof Set)) {
            return joinPoint.proceed();
        }
        // 如果目标参数长度小于拆分下限,跳过
        boolean notMeetSplitLen = (needSplitParam instanceof Object[] && ((Object[]) needSplitParam).length < splitLimit)
                || (needSplitParam instanceof List && ((List) needSplitParam).size() < splitLimit)
                || (needSplitParam instanceof Set && ((Set) needSplitParam).size() < splitLimit);
        if (notMeetSplitLen) {
            return joinPoint.proceed();
        }

        // 4.去重(可选),这一步可根据情况确认是否需要
        if (needSplitParam instanceof List) {
            List<?> listNeedSplitParam = (List<?>) needSplitParam;
            if (listNeedSplitParam.size() > 1) {
                // 将List转为Set,利用Set自动去除重复元素
                HashSet<?> setNeedSplitParam = new HashSet<>(listNeedSplitParam);
                // 重新转为List
                needSplitParam = new ArrayList<>(setNeedSplitParam);
            }
        }

        // 5.算出拆分成几批次
        int getBatchNum = this.getBatchNum(needSplitParam, splitGroupNum);
        if (getBatchNum == 1) {
            return joinPoint.proceed();
        }

        // 6.创建线程池
        ThreadPoolConfig threadPoolConfig = new ThreadPoolConfig(5);
        ExecutorService threadPool = threadPoolConfig.createThreadPool();
        if (threadPool == null) {
            return joinPoint.proceed();
        }

        // 7.开始分批:初始化CompletableFuture数组
        // futures[i]存放第i批任务的执行结果
        CompletableFuture<?>[] futures = new CompletableFuture[getBatchNum];
        // 构建每一批的并发任务
        try {
            for (int currentBatch = 0; currentBatch < getBatchNum; currentBatch++) {
                // 由于异步函数中无法引用非final的变量,因此需要记录下来
                int finalNeedSplitParamIndex = needSplitParamIndex;
                int finalCurrentBatch = currentBatch;
                Object finalNeedSplitParam = needSplitParam;
                // 由于splitGroupNum没有修改,可不需要重新赋值,为了可读性,可重新赋值

                futures[currentBatch] = CompletableFuture.supplyAsync(() -> {
                    // 7.1复制方法参数数组args到新数组
                    // 多线程不能共享原始参数引用,必须拷贝,否则线程之间会修改同一个对象,导致错误
                    Object[] dest = new Object[args.length];
                    // 复制:避免多个线程共享同一个args[]数组,造成线程安全问题
                    System.arraycopy(args, 0, dest, 0, args.length);
                    // 7.2返回当前批次应该处理的那一小部分集合
                    try {
                        dest[finalNeedSplitParamIndex] = this.getBatchParam(finalNeedSplitParam, splitGroupNum, finalCurrentBatch);
                        // 替换方法的参数(比如换成一部分集合),否则拆分没有意义:
                        return joinPoint.proceed(dest);
                    } catch (Throwable e) {
                        throw new RuntimeException(e);
                    }
                }, threadPool);
            }

            // 8.阻塞等待所有任务完成
            CompletableFuture<Void> allOf = CompletableFuture.allOf(futures);
            // get()会等待所有futures[i]完成
            allOf.get();

            // 9.收集每个批次的结果
            // 利用自定义注解中的处理类:对所有分批返回结果进行统一处理和合并,返回一个最终结果
            Class<? extends HandleReturn> aClass = splitWorkAnnotation.handlerReturnClass();
            List<Object> resultList = new ArrayList<>(futures.length);
            for (CompletableFuture<?> future : futures) {
                resultList.add(future.get());
            }
            // 处理返回的核心:
            // 利用反射机制获取构造函数,然后调newInstance方法创建对象
            HandleReturn handleReturnInstance = aClass.getDeclaredConstructor().newInstance();
            // 获取这个类中的第一个方法。注意:这种方式默认调用第一个声明的方法,不是很安全,如果后面你加了其他方法,可能会出错。建议用反射精确找方法名更安全。
//        Method firstMethod = aClass.getDeclaredMethods()[0];
//        firstMethod.invoke(handleReturnInstance, resultList);
            Method handleReturn = aClass.getDeclaredMethod("handleReturn", List.class);
            // 返回聚合后的最终结果
            return handleReturn.invoke(handleReturnInstance, resultList);
        } catch (Exception e) {
           throw new RuntimeException(e);
        }
    }

    // 计算批次数
    private Integer getBatchNum(Object needSplitParam, Integer splitGroupNum) {
        if (needSplitParam instanceof Object[]) {
            Object[] splitParam = (Object[]) needSplitParam;
            // 用于计算将一个数组或字符串分割成若干组后,有几个组(批次)
            // 比如:长度为20,分割组大小为6,则返回3+1=4组
            return splitParam.length % splitGroupNum == 0 ? splitParam.length / splitGroupNum : splitParam.length / splitGroupNum + 1;
        } else if (needSplitParam instanceof Collection) {
            Collection<?> splitParam = (Collection<?>) needSplitParam;
            return splitParam.size() % splitGroupNum == 0 ? splitParam.size() / splitGroupNum : splitParam.size() / splitGroupNum + 1;
        } else {
            return 1;
        }
    }

    /**
     * 获取当前批次参数内容
     *
     * @param needSplitParam 需要分割的对象,只支持数组、List、Set
     * @param splitGroupNum  每组大小
     * @param batchNum       第几批(组)从0开始
     * @return 内容
     */
    private Object getBatchParam(Object needSplitParam, Integer splitGroupNum, Integer batchNum) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        if (needSplitParam instanceof Object[]) {
            Object[] splitParam = (Object[]) needSplitParam;
            // 使用Math.min防止越界
            int end = Math.min((batchNum + 1) * splitGroupNum, splitParam.length);
            return Arrays.copyOfRange(splitParam, batchNum * splitGroupNum, end);
            /*
             copyOfRange方法:从给定数组中复制指定范围的一部分元素,生成一个新数组
             第二个参数from:起始索引(包含)
             第三个参数to:结束索引(不包含)
             */
        } else if (needSplitParam instanceof List) {
            List<?> splitParam = (List<?>) needSplitParam;
            int end = Math.min((batchNum + 1) * splitGroupNum, splitParam.size());
            return splitParam.subList(batchNum * splitGroupNum, end);
            // subList会修改原数据,包含start,不包含end
        } else if (needSplitParam instanceof Set) {
            // Set没有索引操作,无法直接截取子集,所以必须先转换为List,再截取
            List listNeedSplitParam = new ArrayList((Set) needSplitParam);
            int end = Math.min((batchNum + 1) * splitGroupNum, listNeedSplitParam.size());
            // 最后为了保持返回值与原类型一致,用反射构造一个新的Set实例
            Set<?> set = (Set<?>) needSplitParam.getClass().getDeclaredConstructor().newInstance();
            set.addAll(listNeedSplitParam.subList(batchNum * splitGroupNum, end));
            return set;
        } else {
            return null;
        }
    }
}

备注:其余启动类、配置文件不需要额外的设计

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小学鸡!

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值