文章目录
实战:针对实际项目中遇到的场景进行开发
一、目的和思路
目的: 用一个注解实现分批处理、并发执行,然后合并结果返回。一个注解指的是自定义注解,然后利用 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;
}
}
}
备注:其余启动类、配置文件不需要额外的设计