自定义注解(切面实现)

最近有一个新的功能要开发:记录http请求的耗时。

我心想:ok,这个需求很简单啊,不就是上下都记录下时间就完事了吗
伪代码

long startTimeMillis = System.currentTimeMillis();
httpService.request(); // 核心代码
long endTimeMillis = System.currentTimeMillis();
long spendMillis = endTimeMillis - startTimeMillis; // 耗时

提交代码,搞定一个任务😁

随着时间发展,有越来越多的系统要对接,每个接口的http请求都要写一撮low low的代码,自己看了怪恶心的🤮

于是我另起炉灶,把埋点的方式抛弃掉,使用spring切面的方式去实现,切面外行人听着可能觉得挺高级的,咱来弱化一下对他的理解。

类自己的方法上的自定义切面不生效java_自定义注解

切面,可以方法执行前or执行后做一些特殊处理。

这样就好办了,我只需要在切面-前置记录开始时间,切面-后置记录结束时间,然后两者相减就完成需求了。

举个例子,定义一下http请求的原方法

@Service
public class HttpService{
    public void request(){
        //核心代码...发起http请求
    }
}

定义一个HttpServiceAspect切面类

@Aspect
@Component
public class HttpServiceAspect {
    /**
     * 声明AOP签名
     * 这里我指定HttpService.request执行时,会被我捕获到
     */
    @Pointcut("execution(* com.whale.data.service.impl.HttpService.request(..))")
    public void pointcut() {
    }

    /**
     * 环绕切入
     *
     * @param joinPoint 切面对象
     * @return 底层方法执行后的返回值
     * @throws Throwable 底层方法抛出的异常
     */
    @Around("pointcut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        long startTimeMillis = System.currentTimeMillis();
        proceed = joinPoint.proceed(); // 执行原方法
        long endTimeMillis = System.currentTimeMillis();
        long spendMillis = endTimeMillis - startTimeMillis; // 耗时
        return proceed;
    }

搞定!任务调用到com.whale.data.service.impl.HttpService.request()的方法都会被我的切面拦截,这样我就不需要在每个系统交互上下去埋点了。(可以开心一会😄)

快乐的日子总是短暂的,成年人的世界里总是需要经历一定的坎坷。这不,新的问题又来了。

假如开发人员新增了一个接口httpService.post(),那上述方式切面的方式就不够用了,我还得在Pointcut里指定新的切点
如:

@Pointcut("execution(* com.whale.data.service.impl.HttpService.request(..)) || execution(* com.whale.data.service.impl.HttpService.post(..)) ")
    public void pointcut() {
    }

切面也长大了,要学会自己去管理切点才行呀!

如果接口耗时功能的入口不在切面,而在方法上,那开发人员就可以根据自己的需要,自己选择是否启用了,这样的动态配置,谁不喜欢呢?

于是乎,我们又进行了稍许改造,引入了自定义注解
跟之前切面的案例一样,我们也进行一次降维打击哈

原流程

类自己的方法上的自定义切面不生效java_自定义注解_02

加入注解,启动切面功能

类自己的方法上的自定义切面不生效java_System_03

ok,看完上图大家对注解应该有个初步的认知,通过注解去实现切面的开关

同样我们上一段代码来看看程序是如何实现的。

定义自定义注解HttpLog

@Target(ElementType.METHOD) // 用于方法上
@Retention(RetentionPolicy.RUNTIME) // 保留到运行时,可通过注解获取
public @interface HttpLog {
    
}

改造切面HttpServiceAspect的切点,使其切入注解

@Aspect
@Component
public class HttpServiceAspect {
    /**
     * 声明AOP签名
     * 这里我指定HttpService.request执行时,会被我捕获到
     */
    @Pointcut("@annotation(com.whale.data.annotate.HttpLog)")
    public void pointcut() {
    }

    /**
     * 环绕切入
     *
     * @param joinPoint 切面对象
     * @return 底层方法执行后的返回值
     * @throws Throwable 底层方法抛出的异常
     */
    @Around("pointcut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        long startTimeMillis = System.currentTimeMillis();
        proceed = joinPoint.proceed(); // 执行原方法
        long endTimeMillis = System.currentTimeMillis();
        long spendMillis = endTimeMillis - startTimeMillis; // 耗时
        return proceed;
    }

以上的满足就已经完成自定义注解了。 接下来看看注解怎么使用

只需要在方法上加入注解@HttpLog即可

@Service
public class HttpService{
    @HttpLog
    public void request(){
        //核心代码...发起http请求
    }
}

举一反三,如果开发新增了post方法也需要统计耗时,那么在post方法上也加上对应的注解即可

@Service
public class HttpService{
    @HttpLog
    public void request(){
        //核心代码...发起http请求
    }
    
    @HttpLog
    public void post(){
        //核心代码...发起http请求
    }
    
    public void get(){
        //核心代码...发起http请求。不使用注解,不会被切面拦截
    }
}

通过上面描述的,大家应该可以知道切面的由来,以及切面和注解的关系了。
切面:无需侵入业务代码,在外层进行特殊的逻辑处理,常用于日志,监控记录。
注解:切面的高级用法