java AOP+ 注解 结合生命周期 的新玩法

259 阅读6分钟

java AOP+ 注解 结合生命周期 的新玩法

相信使用大伙都使用过AOP来进行一些耗时,异常统计。也使用过AOP+注解来实现一些参数校验,权限控制等操作。那我最近接触了公司的一个大数据中间件,可以很方便的将统计数据进行图标展示,同时还能配置预警,但因为这个中间件并没有提供一个非常方便的注解来快速完成收集功能,在过去代码大部分是这样编写的

public void test(){
    HashMap<String, Object> tags = new HashMap<>();
    tags.put("methodName", "test");
    tags.put("className", "Main");
    tags.put("env", "dev");
    Long now = System.currentTimeMillis();
    // 执行业务方法
    try {
        System.out.println("执行业务方法");
    } catch (Exception e) {
        tags.put("timeCost", System.currentTimeMillis() - now);
        tags.put("result", "error");
        throw e;
    }
    tags.put("timeCost", System.currentTimeMillis() - now);
    tags.put("result", "success");
    // 数据发送
    CollectClient.send(tags);
}

这样就可以进行方法名,类名,耗时,环境以及结果的统计。中台会提供表格进行数据的展示,可以进行过滤以及分组,然后就可以看到各个统计维度的次数,比如查看qa环境下methodName == test 的 result 结果为error 的次数 以及耗时。但是采集的过程中对方法有很大的侵入性,那就需要设计一个aop来进行一键采集。


常规做法

自定义一个注解,然后注解里面添加一个枚举数组,需要哪些采集参数就配置上去

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public  @interface TrendCollect {
    TrendKeyEnum[] value();
}
public enum TrendKeyEnum {
    ENV,
​
    CLASS_NAME,
​
    METHOD_NAME,
​
    TIME_COST,
​
    RESULT,
    ;
}

很快啊,劈里啪啦就整好了,然后就是aop,因为采集指标就这些,那我就直接if else来整

屎山

@Aspect
public class TrendAspect {
    @Pointcut("@annotation(com.dct.springbootdemo.demos.trend.TrendCollect)")
    public void trendPointCut() {
    }
​
    @Around("trendPointCut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        // 获取方法签名
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
​
        // 获取方法上的 TrendCollect 注解
        TrendCollect trendCollectAnnotation = method.getDeclaredAnnotation(TrendCollect.class);
        if(trendCollectAnnotation == null){
            return joinPoint.proceed();
        }
        HashMap<String, Object> map = new HashMap<>();
        TrendKeyEnum[] collectKey = trendCollectAnnotation.value();
        boolean needTimeCost = false;
        boolean needResult = false;
        for (TrendKeyEnum trendKeyEnum : collectKey) {
            if(trendKeyEnum == TrendKeyEnum.ENV) {
                // 这里根据项目框架工具设置,这里仅作演示
                map.put("env", "dev");
            }
            
            else if(trendKeyEnum == TrendKeyEnum.CLASS_NAME) {
                String className = joinPoint.getTarget().getClass().getSimpleName();
                map.put("className", className);
            }
            
            else if (trendKeyEnum == TrendKeyEnum.METHOD_NAME) {
                MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
                Method method1 = methodSignature.getMethod();
                map.put("methodName", method1.getName());
            }
            
            else if (trendKeyEnum == TrendKeyEnum.TIME_COST) {
                needTimeCost = true;
            }
            
            else if (trendKeyEnum == TrendKeyEnum.RESULT) {
                needResult = true;
            }
        }
        
        long startTime = System.currentTimeMillis();
        try {
            Object result = joinPoint.proceed();
            if(needTimeCost) {
                map.put("timeCost", System.currentTimeMillis() - startTime);
            }
            if(needResult) {
                map.put("result", "success");
            }
            return result;
        } catch (Exception e) {
            if (needTimeCost) {
                map.put("timeCost", System.currentTimeMillis() - startTime);
            }
            if (needResult) {
                map.put("result", "failed");
            }
            throw e;
        }
    }
}

代码写完了,功能好像也没毛病,但是这一点都不优雅啊,然后拓展性也非常的差,比如有的方法想添加一个只有这个方法才有的key,那我就需要将这个key添加进入枚举,同时还需要在aspect 中对这个新的枚举进行if判断,那有的人肯定说可以用策略模式优化下

那接下来就是策略模式的优化代码

策略上下文

@Component
public class StrategyContext  implements ApplicationContextAware {
    private CollectStrategy collectStrategy;
​
    private ApplicationContext applicationContext;
​
    private EnumMap<TrendKeyEnum, CollectStrategy> serviceContext = new EnumMap<>(TrendKeyEnum.class);
​
    public void handle(Map map, ProceedingJoinPoint joinPoint, TrendKeyEnum trendKeyEnum) {
        CollectStrategy collectStrategy = serviceContext.get(trendKeyEnum);
        collectStrategy.collect(map, joinPoint);
    }
​
​
​
    // 初始化所有的服务Bean,注册在map中
    @PostConstruct
    public void initService() {
        List<CollectStrategy> collect = Stream.of(applicationContext.getBeanNamesForType(CollectStrategy.class))
                .map(name -> applicationContext.getBean(name, CollectStrategy.class))
                .collect(Collectors.toList());
        for (CollectStrategy strategy : collect) {
            serviceContext.put(strategy.getStrategyEnum(), strategy);
        }
    }
​
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}

策略接口

public interface CollectStrategy {
    void collect(Map map, ProceedingJoinPoint joinPoint);
​
    TrendKeyEnum getStrategyEnum();
}

策略实现类,这里仅作展示,就写两个采集的策略

@Component
public class MethodNameStrategy implements CollectStrategy {
    @Override
    public void collect(Map map, ProceedingJoinPoint joinPoint) {
        System.out.println("方法名统计");
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method1 = methodSignature.getMethod();
        map.put("methodName", method1.getName());
    }
​
    @Override
    public TrendKeyEnum getStrategyEnum() {
        return TrendKeyEnum.METHOD_NAME;
    }
}
@Component
public class TimeCostStrategy implements CollectStrategy {
    @Override
    public void collect(Map map, ProceedingJoinPoint joinPoint) {
        System.out.println("耗时统计");
​
        map.compute("timeCost", (key, value) -> {
            long currentTime = System.currentTimeMillis();
            return (value == null) ? currentTime : currentTime - (Long) value;
        });
​
        System.out.println("方法耗时: " + map.get("timeCost") + " 毫秒");
    }
​
    @Override
    public TrendKeyEnum getStrategyEnum() {
        return TrendKeyEnum.TIME_COST;
    }
}

那么接下来AOP就被改为了以下代码

@Aspect
public class TrendAspect {
​
    @Autowired
    StrategyContext strategyContext;
​
    @Pointcut("@annotation(com.dct.springbootdemo.demos.trend.TrendCollect)")
    public void trendPointCut() {
    }
​
    @Around("trendPointCut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        // 获取方法签名
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
​
        // 获取方法上的 TrendCollect 注解
        TrendCollect trendCollectAnnotation = method.getDeclaredAnnotation(TrendCollect.class);
        if(trendCollectAnnotation == null){
            return joinPoint.proceed();
        }
        HashMap<String, Object> map = new HashMap<>();
        TrendKeyEnum[] collectKey = trendCollectAnnotation.value();
        boolean needTimeCost = false;
        for (TrendKeyEnum trendKeyEnum : collectKey) {
            strategyContext.handle(map, joinPoint, trendKeyEnum);
            if(trendKeyEnum == TrendKeyEnum.TIME_COST) {
                needTimeCost = true;
            }
        }
        try {
            Object result = joinPoint.proceed();
            if(needTimeCost) {
                strategyContext.handle(map, joinPoint, TrendKeyEnum.TIME_COST);
            }
            return result;
        } catch (Exception e) {
            if(needTimeCost) {
                strategyContext.handle(map, joinPoint, TrendKeyEnum.TIME_COST);
            }
            throw e;
        }
    }
}

现在是稍微好一些了,但是明显看到还是有很多判断,而这些判断主要是因为耗时,结果返回是需要方法执行完成后再执行的,不能一次处理完。那么接下来就回归到标题的生命周期上来了,解决这个问题也非常简单,就是设计生命周期来处理这些问题。

以下是当前最终应用的设计

生命周期接口

public interface TrendHandle {
    // 方法执行前同步执行
    default void methodBefore(Map<String, Object> map, ProceedingJoinPoint joinPoint){};
​
    // 方法执行后同步执行, 在finally块执行
    default void methodAfter(Map<String, Object> map,ProceedingJoinPoint joinPoint) {}
​
    // 方法执行异常后同步执行, 在catch块执行
    default void methodException(Map<String, Object> map,ProceedingJoinPoint joinPoint, Throwable e){}
    
}

改造枚举类,将枚举对接口进行实现,这样就可以减少策略类添加,这实际上也是策略模式的设计,每个枚举只需要重写特定的钩子函数就行,不需要全部重写

public enum TrendKeyEnum implements TrendHandle {
​
    CLASS_NAME("className") {
        @Override
        public void methodAfter(Map<String, Object> map, ProceedingJoinPoint joinPoint) {
            String className = joinPoint.getTarget().getClass().getSimpleName();
            map.put("className", className);
        }
    },
    METHOD_NAME("methodName") {
        @Override
        public void methodAfter(Map<String, Object> map, ProceedingJoinPoint joinPoint) {
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            Method method = signature.getMethod();
            map.put("methodName", method.getName());
        }
​
    },
    TIME_COST("timeCost") {
        @Override
        public void methodBefore(Map<String, Object> map, ProceedingJoinPoint joinPoint) {
            map.put("timeCost", System.currentTimeMillis());
        }
​
        @Override
        public void methodAfter(Map<String, Object> map, ProceedingJoinPoint joinPoint) {
            map.put("timeCost", System.currentTimeMillis() - (Long) map.get("timeCost"));
        }
    },
​
    RESULT("result") {
        @Override
        public void methodAfter(Map<String, Object> map, ProceedingJoinPoint joinPoint) {
            map.put("result", "success");
        }
​
        @Override
        public void methodException(Map<String, Object> map, ProceedingJoinPoint joinPoint, Throwable e) {
            map.put("result", "failed");
        }
    }
;
    private final String value;
​
    TrendKeyEnum(String value) {
        this.value = value;
    }
​
    public String getValue() {
        return value;
    }
}

对注解添加一个自定义class,只需要符合生命周期接口就行

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public  @interface TrendCollect {
    TrendKeyEnum[] value();
​
    Class<? extends TrendHandle> custom() default TrendHandle.class;
​
}

重点来了,aop中只需要生命周期内执行接口中的钩子函数就行,同时不要忘记是否有自定义类,然后也执行自定义类的生命周期钩子函数

@Around("trendPointCut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
    // 获取方法签名
    MethodSignature signature = (MethodSignature) joinPoint.getSignature();
    Method method = signature.getMethod();
​
    // 获取方法上的 TrendCollect 注解
    TrendCollect trendCollectAnnotation = method.getDeclaredAnnotation(TrendCollect.class);
    if(trendCollectAnnotation == null){
        return joinPoint.proceed();
    }
    Map<String, Object> tags = new HashMap<>();
    TrendKeyEnum[] value = trendCollectAnnotation.value();
    Class<? extends TrendHandle> custom = trendCollectAnnotation.custom();
    TrendHandle customBean = null;
    // 执行前置处理
    for (TrendKeyEnum trendKeyEnum : value) {
        trendKeyEnum.methodBefore(tags, joinPoint);
    }
    // 是否为实现类而非默认接口类型
    if(!custom.isInterface() && TrendHandle.class.isAssignableFrom(custom)){
        // 通过spring上下文获取bean
        customBean = SpringContextUtil.getBean(custom);
    }
    if(customBean != null){
        // 执行自定义处理
        customBean.methodBefore(tags, joinPoint);
    }
    Object result = null;
    try {
        // 执行目标方法
        result = joinPoint.proceed();
    } catch (Exception e) {
        // 异常处理
        for (TrendKeyEnum trendKeyEnum : value) {
            trendKeyEnum.methodException(tags, joinPoint, e);
        }
        if(customBean != null){
            customBean.methodException(tags, joinPoint,e);
        }
        // 直接抛出异常,交给上一级处理
        throw e;
    }finally {
        // 方法后置处理
        for (TrendKeyEnum trendKeyEnum : value) {
            trendKeyEnum.methodAfter(tags, joinPoint);
        }
        if(customBean != null){
            customBean.methodAfter(tags, joinPoint);
        }
        // 发送监控数据
        CollectClient.send(tags);
    }
    // 返回目标方法的执行结果
    return result;
}

准备就绪,开始测试

添加一个自定义类(写完这篇文章刚好是星期五,周末快乐捏)

@Component
public class MyCustom implements TrendHandle {
    @Override
    public void methodBefore(Map<String, Object> map, ProceedingJoinPoint joinPoint) {
        map.put("crazyFriday","vivo 50");
    }
}

测试类

@Component
public class MyTest {
    @TrendCollect(value = {TrendKeyEnum.CLASS_NAME, TrendKeyEnum.METHOD_NAME, TrendKeyEnum.TIME_COST},custom = MyCustom.class)
    public void test(){
        try {
            System.out.println("业务执行");
            Thread.sleep(500);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

打印结果

业务执行
数据发送:[methodName=test, className=MyTest, timeCost=523, crazyFriday=vivo 50]

写在最后

整个的设计其实一两个小时就已经写完了,肯定还有很多考虑不到的地方,还望指正。其实功能实现也是非常简单,我目前接触的业务场景还不多,你也可以纯粹当作一个小玩具。当前注解还是无法与方法体内部进行交互设置值,所以还是只能应用比较浅的方面。