AOP介绍与SpringBoot中实战运用

一、AOP之基础理论

1.1 什么是AOP

在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程(aop)。即将复杂的需求分解出不同方面,将散布在系统中的公共功能集中解决,在不改变原程序的基础上对代码段进行增强处理,增加新的功能。

1.2 AOP实现原理

AOP分为静态AOP和动态AOP。
静态AOP是指AspectJ实现的AOP,在编译期、类加载期织入。
动态AOP是指运行时生成代理对象来织入(Spring AOP的jdk动态代理和cglib代理)。

1.3 AOP相关术语

Aspect(切面)
aspect 由 pointcountadvice 组成, 它既包含了横切逻辑的定义, 也包括了连接点的定义。
AOP的工作重心在于如何将增强织入目标对象的连接点上, 这里包含两个工作:

  • 如何通过 pointcutadvice定位到特定的 joinpoint
  • 如何在 advice 中编写切面代码.

可以简单地认为, 使用 @Aspect注解的类就是切面.

advice(增强/通知)
aspect 添加到特定的 join point(即满足 point cut 规则的 join point) 的一段代码.

连接点(join point)
程序运行中的一些时间点, 例如一个方法的执行, 或者是一个异常的处理.

切点(point cut)
Advice 是和特定的 point cut关联的, 并且在 point cut相匹配的 join point 中执行.
在 Spring 中, 所有的方法都可以认为是 joinpoint, 但是我们并不希望在所有的方法上都添加 Advice, 而 pointcut的作用就是提供一组规则(使用 AspectJ pointcut expression language 来描述) 来匹配joinpoint, 给满足规则的 joinpoint添加 Advice.

关于join point 和 point cut 的区别
在 Spring AOP 中, 所有的方法执行都是 join point. 而 point cut 是一个描述信息, 它修饰的是 join point, 通过 point cut, 我们就可以确定哪些 join point 可以被织入 Advice. 因此 join pointpoint cut 本质上就是两个不同纬度上的东西.
advice 是在 join point 上执行的, 而 point cut 规定了哪些join point 可以执行哪些 advice

目标对象(Target)
织入 advice的目标对象. 目标对象也被称为 advised object.

织入(Weaving)
aspect和其他对象连接起来, 并创建 adviced object 的过程.
根据不同的实现技术, AOP织入有三种方式:

  • 编译器织入, 这要求有特殊的Java编译器.
  • 类装载期织入, 这需要有特殊的类装载器.
  • 动态代理织入, 在运行期为目标类添加增强(Advice)生成子类的方式.

Spring 采用动态代理织入, 而AspectJ采用编译器织入和类装载期织入.

1.4 advice 的类型

before advice ,在 join point 前被执行的 advice. 虽然 before advice 是在 join point 前被执行, 但是它并不能够阻止 join point 的执行, 除非发生了异常

after return advice, 在一个 join point 正常返回后执行的 advice

after throwing advice, 当一个 join point 抛出异常后执行的 advice

after(final) advice, 无论一个 join point 是正常退出还是发生了异常, 都会被执行的 advice.

around advice, 在 join point 前和 joint point 退出后都执行的 advice. 这个是最常用的 advice.

二、@AspectJ 支持

2.1 定义 aspect(切面)

@Component
@Aspect
public class MyTest {
}

注意, 仅仅使用@Aspect 注解, 并不能将一个 Java 对象转换为 Bean, 因此我们还需要使用类似 @Component 之类的注解。
注意, 如果一个 类被@Aspect 标注, 则这个类就不能是其他 aspect 的 **advised object** 了, 因为使用 @Aspect 后, 这个类就会被排除在 auto-proxying 机制之外.

2.2 声明 pointcut

一个 pointcut 的声明由两部分组成:

  • 一个方法签名, 包括方法名和相关参数
  • 一个 pointcut表达式, 用来指定哪些方法执行是我们感兴趣的(即因此可以织入 advice).

在@AspectJ 风格的 AOP 中, 我们使用一个方法来描述 pointcut, 即:

@Pointcut("execution(* com.xys.service.UserService.*(..))") // 切点表达式
private void dataAccessOperation() {

} // 切点前面

这个方法必须无返回值.
上面我们简单地定义了一个pointcut, 这个pointcut 所描述的是: 匹配所有在包 com.xys.service.UserService 下的所有方法的执行.

2.3 切点标志符(designator)

AspectJ5 的切点表达式由标志符(designator)操作参数组成. 如 “execution( greetTo(…))” 的切点表达式, execution就是 标志符, 而圆括号里的 greetTo(..)就是操作参数

  • execution
    匹配 join point 的执行, 例如 "execution(* hello(..))" 表示匹配所有目标类中的 hello() 方法. 这个是最基本的 pointcut标志符.

  • within
    匹配特定包下的所有 join point, 例如 within(com.xys.*)表示 com.xys 包中的所有连接点, 即包中的所有类的所有方法. 而 within(com.xys.service.*Service) 表示在 com.xys.service 包中所有以 Service 结尾的类的所有的连接点.

  • bean
    匹配 bean 名字为指定值的 bean 下的所有方法, 例如:

bean(*Service) // 匹配名字后缀为 Service 的 bean 下的所有方法
bean(myService) // 匹配名字为 myService 的 bean 下的所有方法
  • args
    匹配参数满足要求的的方法.
@Pointcut("within(com.xys.demo2.*)")
public void pointcut2() {
}

@Before(value = "pointcut2()  &&  args(name)")
public void doSomething(String name) {
    logger.info("---page: {}---", name);
}
@Service
public class NormalService {
    private Logger logger = LoggerFactory.getLogger(getClass());

    public void someMethod() {
        logger.info("---NormalService: someMethod invoked---");
    }


    public String test(String name) {
        logger.info("---NormalService: test invoked---");
        return "服务一切正常";
    }
}

当 NormalService.test 执行时, 则 advice doSomething 就会执行,test 方法的参数 name就会传递到 doSomething 中.

  • @annotation

匹配由指定注解所标注的方法, 例如:

@Pointcut("@annotation(com.xys.demo1.AuthChecker)")
public void pointcut() {

}

则匹配由注解 AuthChecker所标注的方法.

三、SpringBoot中AOP实战

3.1使用aop来完成全局请求日志处理

3.1.1 导入依赖
3.1.2 创建一个 Controller

@RestController  
public class FirstController {  
  
    @RequestMapping("/first")  
    public Object first() {  
        return "first controller";  
    }  
  
    @RequestMapping("/doError")  
    public Object error() {  
        return 1 / 0;  
    }  
}  

3.1.3 创建一个aspect切面类

@Aspect  
@Component  
public class LogAspect {  
    @Pointcut("execution(public * com.example.controller.*.*(..))")  
    public void webLog(){}  
  
    @Before("webLog()")  
    public void deBefore(JoinPoint joinPoint) throws Throwable {  
        // 接收到请求,记录请求内容  
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();  
        HttpServletRequest request = attributes.getRequest();  
        // 记录下请求内容  
        System.out.println("URL : " + request.getRequestURL().toString());  
        System.out.println("HTTP_METHOD : " + request.getMethod());  
        System.out.println("IP : " + request.getRemoteAddr());  
        System.out.println("CLASS_METHOD : " + joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());  
        System.out.println("ARGS : " + Arrays.toString(joinPoint.getArgs()));  
  
    }  
  
    @AfterReturning(returning = "ret", pointcut = "webLog()")  
    public void doAfterReturning(Object ret) throws Throwable {  
        // 处理完请求,返回内容  
        System.out.println("方法的返回值 : " + ret);  
    }  
  
    //后置异常通知  
    @AfterThrowing("webLog()")  
    public void throwss(JoinPoint jp){  
        System.out.println("方法异常时执行.....");  
    }  
  
    //后置最终通知,final增强,不管是抛出异常或者正常退出都会执行  
    @After("webLog()")  
    public void after(JoinPoint jp){  
        System.out.println("方法最后执行.....");  
    }  
  
    //环绕通知,环绕增强,相当于MethodInterceptor  
    @Around("webLog()")  
    public Object arround(ProceedingJoinPoint pjp) {  
        System.out.println("方法环绕start.....");  
        try {  
            Object o =  pjp.proceed();  
            System.out.println("方法环绕proceed,结果是 :" + o);  
            return o;  
        } catch (Throwable e) {  
            e.printStackTrace();  
            return null;  
        }  
    }  

3.1.4 启动项目
访问http://localhost:8080/first,看控制台结果:

方法环绕start.....
URL : http://localhost:8080/first
HTTP_METHOD : GET
IP : 0:0:0:0:0:0:0:1
CLASS_METHOD : com.example.controller.FirstController.first
ARGS : []
方法环绕proceed,结果是 :first controller
方法最后执行.....
方法的返回值 : first controller

模拟出现异常时的情况,访问http://localhost:8080/doError,看控制台结果:

方法环绕start.....
URL : http://localhost:8080/doError
HTTP_METHOD : GET
IP : 0:0:0:0:0:0:0:1
CLASS_METHOD : com.example.controller.FirstController.error
ARGS : []
java.lang.ArithmeticException: / by zero
....
方法最后执行.....
方法的返回值 : null

3.2 Http接口鉴权

设计思路:

  • 提供一个特殊的注解 AuthChecker, 这个是一个方法注解, 有此注解所标注的 Controller需要进行调用方权限的认证.
  • 利用 AOP, 以 @annotation切点标志符来匹配有注解 AuthChecker 所标注的 joinpoint.
  • advice 中, 简单地检查调用者请求中的Cookie中是否有我们指定的 token, 如果有, 则认为此调用者权限合法,允许调用, 反之权限不合法, 范围错误.

3.2.1 导入依赖
3.2.2 创建注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AuthChecker {
}

3.2.3 切面类

@Component
@Aspect
public class HttpAopAdviseDefine {

    // 定义一个 Pointcut, 使用 切点表达式函数 来描述对哪些 Join point 使用 advise.
    @Pointcut("@annotation(com.xys.demo1.AuthChecker)")
    public void pointcut() {
    }

    // 定义 advise
    @Around("pointcut()")
    public Object checkAuth(ProceedingJoinPoint joinPoint) throws Throwable {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
                .getRequest();

        // 检查用户所传递的 token 是否合法
        String token = getUserToken(request);
        if (!token.equalsIgnoreCase("123456")) {
            return "错误, 权限不合法!";
        }

        return joinPoint.proceed();
    }

    private String getUserToken(HttpServletRequest request) {
        Cookie[] cookies = request.getCookies();
        if (cookies == null) {
            return "";
        }
        for (Cookie cookie : cookies) {
            if (cookie.getName().equalsIgnoreCase("user_token")) {
                return cookie.getValue();
            }
        }
        return "";
    }
}

3.2.4 创建Http接口进行模拟(Controller)

@RestController
public class DemoController {
    @RequestMapping("/aop/http/alive")
    public String alive() {
        return "服务一切正常";
    }

    @AuthChecker
    @RequestMapping("/aop/http/user_info")
    public String callSomeInterface() {
        return "调用了 user_info 接口.";
    }
}

3.3 方法耗时统计

设计:
首先我们可以使用 around advice, 然后在方法调用前, 记录一下开始时间, 然后在方法调用结束后, 记录结束时间, 它们的时间差就是方法的调用耗时.

切面类:

@Component
@Aspect
public class ExpiredAopAdviseDefine {
    private Logger logger = LoggerFactory.getLogger(getClass());

    // 定义一个 Pointcut, 使用 切点表达式函数 来描述对哪些 Join point 使用 advise.
    @Pointcut("within(SomeService)")
    public void pointcut() {
    }

    // 定义 advise
    // 定义 advise
    @Around("pointcut()")
    public Object methodInvokeExpiredTime(ProceedingJoinPoint pjp) throws Throwable {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        // 开始
        Object retVal = pjp.proceed();
        stopWatch.stop();
        // 结束

        // 上报到公司监控平台
        reportToMonitorSystem(pjp.getSignature().toShortString(), stopWatch.getTotalTimeMillis());

        return retVal;
    }


    public void reportToMonitorSystem(String methodName, long expiredTime) {
        logger.info("---method {} invoked, expired time: {} ms---", methodName, expiredTime);
        //
    }
}

借鉴:
https://www.cnblogs.com/bigben0123/p/7779357.html
https://segmentfault.com/a/1190000007469982
https://segmentfault.com/a/1190000007469968

猜你喜欢

转载自blog.csdn.net/qq_43386941/article/details/105901000
今日推荐