Spring AOP(二)AOP配合自定义注解应用

689 阅读10分钟

Spring AOP(二)AOP配合自定义注解应用

在了解AOP的核心概念后,结合自定义注解我们就可以很灵活的做到在原有代码的基础上进行逻辑的扩展,这个文章讲解在springboot中通过aop+自定义注解的方式,实现面向切面编程项目地址

代码展示

在了解AOP的核心概念之后,这篇文章结合demo案例来了解实际中的应用,其实实现面相切面的方式很多但是结合实际中常用的方式,本次主要介绍两种方式:
1.springboot中基于interceptor和自定义注解形式;
2.springboot中基于@Aspect和自定义注解形式;

基于Interceptor和自定义注解形式

注解类LoggerAnnotation

package com.troyqu.annotation.aop.interceptor.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @Author 
 * @Date 2021/9/6
 * @Time 下午5:39
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoggerAnnotation {

}

spring interceptor配置
config配置

package com.troyqu.annotation.aop.interceptor.config;

import com.troyqu.annotation.aop.interceptor.interceptor.LoggerInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.handler.WebRequestHandlerInterceptorAdapter;

/**
 * @Author 
 * @Date 2021/9/6
 * @Time 下午5:59
 */
@Configuration
public class TryAnnotationConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //通过PathPatterns配置interceptor的生效范围,如果不添加默认会拦截所有请求,所以最好结合实际使用情况配置相应的path
        registry.addInterceptor(new WebRequestHandlerInterceptorAdapter(new LoggerInterceptor())).addPathPatterns("/test", "/login");
        
    }
}

interceptor

package com.troyqu.annotation.aop.interceptor.interceptor;

import com.troyqu.annotation.aop.interceptor.annotation.LoggerAnnotation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.ui.ModelMap;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.context.request.WebRequestInterceptor;
import org.springframework.web.method.HandlerMethod;

import java.lang.reflect.Method;

/**
 * @Author 
 * @Date 2021/9/6
 * @Time 下午5:41
 */
@Component
public class LoggerInterceptor implements WebRequestInterceptor {

    private Logger logger = LoggerFactory.getLogger(LoggerInterceptor.class);

//    springboot 2.1.x版本中处理preHandler和postHandler的方式
//    @Override
//    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//        HandlerMethod handlerMethod = (HandlerMethod)handler;
//        Method method = handlerMethod.getMethod();
//        //获取当前方法上的指定注解
//        LoggerAnnotation loggerAnnotation = method.getAnnotation(LoggerAnnotation.class);
//        //判断当前注解是否存在
//        if(loggerAnnotation != null){
//            long startTime = System.currentTimeMillis();
//            request.setAttribute("startTime",startTime);
//            logger.info("进入" + method.getName() + "方法的时间是:" + startTime);
//        }
//        return true;
//    }
//
//    @Override
//    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
//        HandlerMethod handlerMethod = (HandlerMethod)handler;
//        Method method = handlerMethod.getMethod();
//        //获取当前方法上的指定注解
//        LoggerAnnotation loggerAnnotation = method.getAnnotation(LoggerAnnotation.class);
//        //判断当前注解是否存在
//        if(loggerAnnotation != null){
//            long endTime = System.currentTimeMillis();
//            long startTime = (Long) request.getAttribute("startTime");
//            long periodTime = endTime - startTime;
//            logger.info("离开" + method.getName() + "方法的时间是:" + endTime);
//            logger.info("在" + method.getName() + "方法的时长是:" + periodTime);
//        }
//    }

    /**
     * springboot 2.5.x版本中处理preHandler和postHandler的方式
     * 缺点:先拦截所有方法,然后进行判断是否方法有LoggerAnnotation注解,不够高效
     *
     * @param request
     * @throws Exception
     */
    @Override
    public void preHandle(WebRequest request) throws Exception {
        logger.info("come in process preHandler");
        //scope=0表示从request中获取属性,scope!=0表示从session中获取属性
        Object object = request.getAttribute("org.springframework.web.servlet.HandlerMapping.bestMatchingHandler", 0);
        if (object instanceof HandlerMethod) {
            HandlerMethod handlerMethod = (HandlerMethod) object;
            Method method = handlerMethod.getMethod();
            LoggerAnnotation annotation = method.getAnnotation(LoggerAnnotation.class);
            if (null != annotation) {
                logger.info("preHandler method " + method.getName() + " with LoggerAnnotation annotation");
            }
        } else {
            logger.info("request handler mapper is not instance of HandlerMethod skip LoggerInterceptor");
        }

    }

    @Override
    public void postHandle(WebRequest request, ModelMap model) throws Exception {
        logger.info("come in process postHandler");

    }

    @Override
    public void afterCompletion(WebRequest request, Exception ex) throws Exception {
        logger.info("come in process afterCompletion");

    }
}

controller示例

package com.troyqu.annotation.aop.interceptor;

import com.troyqu.annotation.aop.interceptor.annotation.LoggerAnnotation;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @Author 
 * @Date 2021/9/6
 * @Time 下午6:02
 */
@RestController
public class LoginController {
    @RequestMapping("/login")
    @LoggerAnnotation
    public void login() {
        try {
            System.out.println("login this time");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @RequestMapping("/test")
    public void test() {
        try {
            System.out.println("test this time");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

运行代码访问http://127.0.0.1:8080/login 查看日志输出可以看到interceptor中我们定义的逻辑已经被处理。

2021-11-24 15:44:33.600  INFO 65161 --- [nio-8080-exec-4] c.t.a.a.i.interceptor.LoggerInterceptor  : come in process preHandler
2021-11-24 15:44:33.600  INFO 65161 --- [nio-8080-exec-4] c.t.a.a.i.interceptor.LoggerInterceptor  : preHandler method login with LoggerAnnotation annotation
login this time
2021-11-24 15:44:33.602  INFO 65161 --- [nio-8080-exec-4] c.t.a.a.i.interceptor.LoggerInterceptor  : come in process postHandler
2021-11-24 15:44:33.602  INFO 65161 --- [nio-8080-exec-4] c.t.a.a.i.interceptor.LoggerInterceptor  : come in process afterCompletion

基于@Aspect和自定义注解形式

使用Aspect和@annotation
自定义注解Troy

package com.troyqu.annotation.aop.aspectj.annotation;

import org.springframework.stereotype.Component;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @Author 
 * @Date 2021/11/11
 * @Time 上午11:55
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Component
public @interface Troy {

}

AOP配置通过JoinPoint可以获取到被AOP命中的targer的类和方法相关信息

package com.troyqu.annotation.aop.aspectj.aop;

import com.troyqu.annotation.aop.aspectj.annotation.Human;
import com.troyqu.annotation.aop.aspectj.annotation.Troy;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

/**
 * 使用Aspect @annotation的方式进行AOP处理
 *
 * @Author 
 * @Date 2021/11/11
 * @Time 上午11:58
 */
@Aspect
@Component
public class TroyAnnotationAop {

    private Logger logger = LoggerFactory.getLogger(TroyAnnotationAop.class);

    @Before(value = "@annotation(troy)")
    public void aspBeforeTroy(JoinPoint joinPoint, Troy troy) {
        logger.info("come in aspBefore {}", joinPoint.getSignature().getName());
        logger.info("come in aspBefore");
    }

    @After(value = "@annotation(troy)")
    public void aspAfterTroy(JoinPoint joinPoint, Troy troy) {
        logger.info("come in aspAfter {}", joinPoint.getSignature());
        logger.info("come in aspAfter");
    }
}

controller入口

package com.troyqu.annotation.aop.aspectj.controller;

import com.troyqu.annotation.aop.aspectj.annotation.Human;
import com.troyqu.annotation.aop.aspectj.annotation.Troy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

/**
 * @Author 
 * @Date 2021/11/11
 * @Time 下午12:04
 */
@RestController
public class AnnotationController {

    private Logger logger = LoggerFactory.getLogger(AnnotationController.class);

    @GetMapping("/asp/test")
    @Troy
    public void test() {
        logger.info("asp test");
    }
}    

启动应用访问http://127.0.0.1:8080/asp/test 通过输出可以看到通过自定义注解配置的AOP生效了,而且我们看到通过joinpoint可以获取到被AOP命中的目标的信息,这样我们后面就可以在AOP的advice中做很多事情,具体情况可以结合实际应用来灵活运用。

2021-11-24 16:08:57.853  INFO 65161 --- [io-8080-exec-10] c.t.a.aop.aspectj.aop.TroyAnnotationAop  : come in aspBefore test
2021-11-24 16:08:57.859  INFO 65161 --- [io-8080-exec-10] c.t.a.aop.aspectj.aop.TroyAnnotationAop  : come in aspBefore
2021-11-24 16:08:57.910  INFO 65161 --- [io-8080-exec-10] c.t.a.a.a.c.AnnotationController         : asp test
2021-11-24 16:08:57.911  INFO 65161 --- [io-8080-exec-10] c.t.a.aop.aspectj.aop.TroyAnnotationAop  : come in aspAfter void com.troyqu.annotation.aop.aspectj.controller.AnnotationController.test()
2021-11-24 16:08:57.911  INFO 65161 --- [io-8080-exec-10] c.t.a.aop.aspectj.aop.TroyAnnotationAop  : come in aspAfter

使用Aspect和@pointcut
注解类Human

package com.troyqu.annotation.aop.aspectj.annotation;

import org.springframework.stereotype.Component;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @Author 
 * @Date 2021/11/11
 * @Time 下午5:01
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Component
public @interface Human {
    String value() default "NO-Name";
}

AOP配置类(基于pointcut方式),

-- AspectAnnotationAop 基于Aspect和pointcut注解 @annotation方式声明的AOP
-- AspectExectionAop 基于Aspect和pointcut注解 execution表达式方式声明的AOP
-- AspectWithinAop 基于Aspect和pointcut注解 @Within方式声明的AOP

采用pointcut和@annotation进行AOP展示

package com.troyqu.annotation.aop.aspectj.aop;

import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

/**
 * 使用Aspect 采用pointcut和@annotation进行AOP展示
 *
 * @Author 
 * @Date 2021/11/11
 * @Time 下午5:04
 */

@Order(2)
@Aspect
@Component
public class AspectAnnotationAop {

    private Logger logger = LoggerFactory.getLogger(AspectAnnotationAop.class);

    @Pointcut("@annotation(com.troyqu.annotation.aop.aspectj.annotation.Human)")
    public void annoPointCut() {
    }

    @Before("annoPointCut()")
    public void beforePointCut() {
        logger.info("come in annoPointCut before");
    }

    @After("annoPointCut()")
    public void afterPointCut() {
        logger.info("come in annoPointCut after");
    }

    @AfterReturning("annoPointCut()")
    public void afterRetPointCut() {
        logger.info("come in annoPointCut AfterReturning");
    }
}

采用execution方式定义pointcut范围的方式

package com.troyqu.annotation.aop.aspectj.aop;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

/**
 * 使用Aspect 采用pointcut配合execution表达式进行AOP示例
 *
 * @Author 
 * @Date 2021/11/11
 * @Time 下午4:45
 */

@Aspect
@Component
public class AspectExecutionAop {

    private Logger logger = LoggerFactory.getLogger(AspectExecutionAop.class);

    /**
     * 只声明一个切点,不做任何逻辑处理
     * <p>
     * execution(): 表达式主体。
     * 一个*号:表示返回类型,*号表示所有的类型。
     * 包名:表示需要拦截的包名,后面的两个句点表示当前包和当前包的所有子包,com.sample.service.impl包、子孙包下所有类的方法。
     * 第二个*号:表示类名,*号表示所有的类。
     * *(…):最后这个星号表示方法名,*号表示所有的方法,后面括弧里面表示方法的参数,两个句点表示任何参数
     */
    @Pointcut("execution(* com.troyqu.annotation.aop.aspectj.execcontroller..*.*(..))")
    public void execPointCut() {
    }

    @Before("execPointCut()")
    public void beforePointCut(JoinPoint joinPoint) {
        logger.info("come in execPointCut before {}", joinPoint.getSignature());
        logger.info("come in execPointCut before");
    }

    @After("execPointCut()")
    public void afterPointCut() {
        logger.info("come in execPointCut after");
    }

    @AfterReturning("execPointCut()")
    public void afterRetPointCut() {
        logger.info("come in execPointCut AfterReturning");
    }
}

采用Within方式定义pointcut范围的方式

package com.troyqu.annotation.aop.aspectj.aop;

import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

/**
 * 使用Aspect 采用pointcut配合within的方式处理AOP
 *
 * @Author 
 * @Date 2021/11/11
 * @Time 下午4:46
 */

@Aspect
@Component
public class AspectWithinAop {

    private Logger logger = LoggerFactory.getLogger(AspectWithinAop.class);

    /**
     * within 处理某一个具体类或者包下的所有方法
     */
    @Pointcut("within(com.troyqu.annotation.aop.aspectj.controller.AspectWithinController)")
    public void withInPointCut() {
    }

    @Before("withInPointCut()")
    public void beforePointCut() {
        logger.info("come in withInPointCut before");
    }

    @After("withInPointCut()")
    public void afterPointCut() {
        logger.info("come in withInPointCut after");
    }

    @AfterReturning("withInPointCut()")
    public void afterRetPointCut() {
        logger.info("come in withInPointCut AfterReturning");
    }

}

controller入口 配合Within方式的AOP切面命中目标的接口入口,通过访问http://127.0.0.1:8080/asp/within/testhttp://127.0.0.1:8080/asp/within/login 接口可以查看示例输出。

package com.troyqu.annotation.aop.aspectj.controller;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @Author 
 * @Date 2021/11/11
 * @Time 下午5:06
 */
@RestController
public class AspectWithinController {

    private Logger logger = LoggerFactory.getLogger(AspectWithinController.class);

    @GetMapping("/asp/within/test")
    public void test() {
        logger.info("asp within test");
    }

    @GetMapping("/asp/within/login")
    public void login() {
        logger.info("asp within login");
    }

}

配合execution方式的AOP切面命中目标的接口入口,通过访问http://127.0.0.1:8080/asp/exec/testhttp://127.0.0.1:8080/asp/exec/login 接口可以查看示例输出。

package com.troyqu.annotation.aop.aspectj.execcontroller;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @Author 
 * @Date 2021/11/11
 * @Time 下午4:48
 */
@RestController
public class AspectExecController {

    private Logger logger = LoggerFactory.getLogger(AspectExecController.class);

    @GetMapping("/asp/exec/test")
    public String test() {
        logger.info("asp exec test");
        return "asp/exec/test";
    }

    @GetMapping("/asp/exec/login")
    public String login() {
        logger.info("asp exec login");
        return "asp/exec/login";
    }
}

配合@annotation方式的AOP切面命中目标的接口入口,通过访问http://127.0.0.1:8080/asp/testHumanhttp://127.0.0.1:8080/asp/testHuman/{name} 接口可以查看示例输出。

package com.troyqu.annotation.aop.aspectj.controller;

import com.troyqu.annotation.aop.aspectj.annotation.Human;
import com.troyqu.annotation.aop.aspectj.annotation.Troy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

/**
 * @Author 
 * @Date 2021/11/11
 * @Time 下午12:04
 */
@RestController
public class AnnotationController {

    private Logger logger = LoggerFactory.getLogger(AnnotationController.class);

    @GetMapping("/asp/testHuman")
    @Human("AI")
    public void testHuman() {
        logger.info("asp testHuman");
    }

    @GetMapping("/asp/testHuman/{name}")
    @Human
    public void testHuman(@PathVariable(value = "name") String name) {
        logger.info("asp testHuman {}", name);
    }
}

在这里就不再逐个展示接口输出内容了,通过一系列的示例,我们目前已经掌握了使用AOP的多种方式,当时我们设想一个场景,当有多个AOP都命中同一个目标的时候,那么那个AOP会被先执行呢?接下来我们就看一下当有多个AOP命中同一个目标的时候,那AOP的执行顺序是怎样的呢,或者说我们应该如何控制?

这里我们可以尝试结合Order接口来看是否会控制AOP的执行顺序, 接下来我们来验证是否可以通过Order来控制执行顺序

现在HumanAnnotationAop和AspectAnnotationAop都会命中AnnotationController的 /asp/testHuman和/asp/testHuman/{name}
场景1:HumanAnnotationAop配置Order(1) AspectAnnotationAop配置Order(2)

2021-11-24 16:35:28.613  INFO 65161 --- [nio-8080-exec-3] c.t.a.a.aspectj.aop.HumanAnnotationAop   : come in aspBeforeHuman without name testHuman
2021-11-24 16:35:28.616  INFO 65161 --- [nio-8080-exec-3] c.t.a.a.aspectj.aop.HumanAnnotationAop   : come in aspBeforeHuman without name 
2021-11-24 16:35:28.619  INFO 65161 --- [nio-8080-exec-3] c.t.a.a.aspectj.aop.AspectAnnotationAop  : come in annoPointCut before
2021-11-24 16:35:28.624  INFO 65161 --- [nio-8080-exec-3] c.t.a.a.a.c.AnnotationController         : asp testHuman
2021-11-24 16:35:28.632  INFO 65161 --- [nio-8080-exec-3] c.t.a.a.aspectj.aop.AspectAnnotationAop  : come in annoPointCut AfterReturning
2021-11-24 16:35:28.632  INFO 65161 --- [nio-8080-exec-3] c.t.a.a.aspectj.aop.AspectAnnotationAop  : come in annoPointCut after
2021-11-24 16:35:28.633  INFO 65161 --- [nio-8080-exec-3] c.t.a.a.aspectj.aop.HumanAnnotationAop   : come in aspAfterHuman without name void com.troyqu.annotation.aop.aspectj.controller.AnnotationController.testHuman()
2021-11-24 16:35:28.633  INFO 65161 --- [nio-8080-exec-3] c.t.a.a.aspectj.aop.HumanAnnotationAop   : come in aspAfterHuman without name human AI

场景2:HumanAnnotationAop配置Order(2) AspectAnnotationAop配置Order(1)

2021-11-24 16:44:03.899  INFO 80261 --- [nio-8080-exec-2] c.t.a.a.aspectj.aop.AspectAnnotationAop  : come in annoPointCut before
2021-11-24 16:44:03.900  INFO 80261 --- [nio-8080-exec-2] c.t.a.a.aspectj.aop.HumanAnnotationAop   : come in aspBeforeHuman without name testHuman
2021-11-24 16:44:03.900  INFO 80261 --- [nio-8080-exec-2] c.t.a.a.aspectj.aop.HumanAnnotationAop   : come in aspBeforeHuman without name 
2021-11-24 16:44:03.900  INFO 80261 --- [nio-8080-exec-2] c.t.a.a.a.c.AnnotationController         : asp testHuman
2021-11-24 16:44:03.900  INFO 80261 --- [nio-8080-exec-2] c.t.a.a.aspectj.aop.HumanAnnotationAop   : come in aspAfterHuman without name void com.troyqu.annotation.aop.aspectj.controller.AnnotationController.testHuman()
2021-11-24 16:44:03.900  INFO 80261 --- [nio-8080-exec-2] c.t.a.a.aspectj.aop.HumanAnnotationAop   : come in aspAfterHuman without name human AI
2021-11-24 16:44:03.900  INFO 80261 --- [nio-8080-exec-2] c.t.a.a.aspectj.aop.AspectAnnotationAop  : come in annoPointCut AfterReturning
2021-11-24 16:44:03.900  INFO 80261 --- [nio-8080-exec-2] c.t.a.a.aspectj.aop.AspectAnnotationAop  : come in annoPointCut after

通过对比我们可以发现我们可以通过Order来控制多个AOP的执行顺序,那么即使遇到多个AOP命中多个目标的情况我们也可以通过Order接口来控制,AOP的执行顺序来确保业务正常。

总结

在springboot中通过自定义注解的方式配合AOP可以很灵活的对代码进行扩展,大大提高了系统的灵活性,但是要注意的是,设计AOP的时候非必要尽量不要让多个AOP命中同一个目标,如果真有需要那么可以配合Order接口来定义执行顺序。