Spring AOP

1.AOP概述

AOP就是面向切面编程,其实就是面向特定的方法编程。基于动态代理的技术,给特定的方法进行功能增强。

举一个常见的例子,比如想要统计系统中每个方法的运行耗时,如果不使用AOP,就必须给每个方法就加上统计运行时长的代码逻辑,这样会很繁琐,且容易漏。如果使用AOP的话只需要在切面类中增加这个逻辑,使用切入点表达式去指定目标方法,就可以给目标方法都加上这一逻辑,实现我们的统计时长的功能。

2.AOP的核心概念

(1)连接点 :JoinPoint 可以被AOP控制的方法 (暗含方法执行时的相关信息)

(2)通知:Advice,指那些重复的逻辑,及公共的功能,都体现在通知方法中。

(3)切入点:PointCut,匹配连接点的条件,通知仅会在切入点方法执行时被应用

(4)切面:Aspect 描述通知与切入点的对应关系(切入点+通知)

(5)目标对象:target,通知所应用的对象

3.AOP的实现原理

SpringAOP实现原理其实很简单,就是通过动态代理实现的。如果我们为Spring的某个bean配置了切面,那么Spring在创建这个bean的时候,实际上创建的是这个bean的一个代理对象,我们后续对bean中方法的调用,实际上调用的是代理类重写的代理方法。而SpringAOP使用了两种动态代理,分别是JDK的动态代理,以及CGLib的动态代理

(一)JDK动态代理

  Spring默认使用JDK的动态代理实现AOP,类如果实现了接口,Spring就会使用这种方式实现动态代理。熟悉Java语言的应该会对JDK动态代理有所了解。JDK实现动态代理需要两个组件,首先第一个就是InvocationHandler接口。我们在使用JDK的动态代理时,需要编写一个类,去实现这个接口,然后重写invoke方法,这个方法其实就是我们提供的代理方法。然后JDK动态代理需要使用的第二个组件就是Proxy这个类,我们可以通过这个类的newProxyInstance方法,返回一个代理对象。生成的代理类实现了原来那个类的所有接口,并对接口的方法进行了代理,我们通过代理对象调用这些方法时,底层将通过反射,调用我们实现的invoke方法。

(二)CGLib动态代理

  JDK的动态代理存在限制,那就是被代理的类必须是一个实现了接口的类,代理类需要实现相同的接口,代理接口中声明的方法。若需要代理的类没有实现接口,此时JDK的动态代理将没有办法使用,于是Spring会使用CGLib的动态代理来生成代理对象。CGLib直接操作字节码,生成类的子类,重写类的方法完成代理。

  以上就是Spring实现动态的两种方式,下面我们具体来谈一谈这两种生成动态代理的方式。

3.1 JDK的动态代理

(一)实现原理

  JDK的动态代理是基于反射实现。JDK通过反射,生成一个代理类,这个代理类实现了原来那个类的全部接口,并对接口中定义的所有方法进行了代理。当我们通过代理对象执行原来那个类的方法时,代理类底层会通过反射机制,回调我们实现的InvocationHandler接口的invoke方法。并且这个代理类是Proxy类的子类(记住这个结论,后面测试要用)。这就是JDK动态代理大致的实现方式。

(二)优点

  1. JDK动态代理是JDK原生的,不需要任何依赖即可使用;
  2. 通过反射机制生成代理类的速度要比CGLib操作字节码生成代理类的速度更快;

(三)缺点

  1. 如果要使用JDK动态代理,被代理的类必须实现了接口,否则无法代理;
  2. JDK动态代理无法为没有在接口中定义的方法实现代理,假设我们有一个实现了接口的类,我们为它的一个不属于接口中的方法配置了切面,Spring仍然会使用JDK的动态代理,但是由于配置了切面的方法不属于接口,为这个方法配置的切面将不会被织入。
  3. JDK动态代理执行代理方法时,需要通过反射机制进行回调,此时方法执行的效率比较低;

3.2 CGLib动态代理

(一)实现原理

  CGLib实现动态代理的原理是,底层采用了ASM字节码生成框架,直接对需要代理的类的字节码进行操作,生成这个类的一个子类,并重写了类的所有可以重写的方法,在重写的过程中,将我们定义的额外的逻辑(简单理解为Spring中的切面)织入到方法中,对方法进行了增强。而通过字节码操作生成的代理类,和我们自己编写并编译后的类没有太大区别。

(二)优点

  1. 使用CGLib代理的类,不需要实现接口,因为CGLib生成的代理类是直接继承自需要被代理的类;
  2. CGLib生成的代理类是原来那个类的子类,这就意味着这个代理类可以为原来那个类中,所有能够被子类重写的方法进行代理;
  3. CGLib生成的代理类,和我们自己编写并编译的类没有太大区别,对方法的调用和直接调用普通类的方式一致,所以CGLib执行代理方法的效率要高于JDK的动态代理;

(三)缺点

  1. 由于CGLib的代理类使用的是继承,这也就意味着如果需要被代理的类是一个final类,则无法使用CGLib代理;
  2. 由于CGLib实现代理方法的方式是重写父类的方法,所以无法对final方法,或者private方法进行代理,因为子类无法重写这些方法;
  3. CGLib生成代理类的方式是通过操作字节码,这种方式生成代理类的速度要比JDK通过反射生成代理类的速度更慢;

4.AOP的通知类型

1.@Around:环绕通知,此注解标注的通知方法在目标方法前、后都被执行
2.@Before:前置通知,此注解标注的通知方法在目标方法前被执行
3.@After :后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行4.@AfterReturning,返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行5.@AfterThrowing :异常后通知,此注解标注的通知方法发生异常后执行

注意事项:

1.@Around 环绕通知需要自己调用 Proceeding]oinPoint.proceed()来让原始方法执行,其他通知不需要考虑目标方法执行

2.@Around 环绕通知方法的返回值,必须指定为0bject,来接收原始方法的返回值。

5. 切入点表达式

Spring AOP 支持以下几种切点表达式类型。

1.execution:

匹配方法切入点。根据表达式描述匹配方法,是最通用的表达式类型,可以匹配方法、类、包。

表达式模式:

execution(modifier? ret-type declaring-type?name-pattern(param-pattern) throws-pattern?)

modifier:匹配修饰符,public, private 等,省略时匹配任意修饰符

ret-type:匹配返回类型,使用 * 匹配任意类型

declaring-type:匹配目标类,省略时匹配任意类型

        .. 匹配包及其子包的所有类
 

name-pattern:匹配方法名称,使用 * 表示通配符

        * 匹配任意方法
        set* 匹配名称以 set 开头的方法
 

param-pattern:匹配参数类型和数量

        () 匹配没有参数的方法
        (..) 匹配有任意数量参数的方法
        (*) 匹配有一个任意类型参数的方法
        (*,String) 匹配有两个参数的方法,并且第一个为任意类型,第二个为 String 类型
 

throws-pattern:匹配抛出异常类型,省略时匹配任意类型

使用示例:

// 匹配public方法
execution(public * *(..))

// 匹配名称以set开头的方法
execution(* set*(..))

// 匹配AccountService接口或类的方法
execution(* com.xyz.service.AccountService.*(..))

// 匹配service包及其子包的类或接口
execution(* com.xyz.service..*(..))

2.within

匹配指定类型。匹配指定类的任意方法,不能匹配接口。

表达式模式:

within(declaring-type)

使用示例

// 匹配service包的类
within(com.xyz.service.*)

// 匹配service包及其子包的类
within(com.xyz.service..*)

// 匹配AccountServiceImpl类
within(com.xyz.service.AccountServiceImpl)

3.this

匹配代理对象实例的类型,匹配在运行时对象的类型。

注意:基于 JDK 动态代理实现的 AOP,this 不能匹配接口的实现类,因为代理类和实现类并不是同一种类型,详情参阅《Spring中的AOP和动态代理

4.target

匹配目标对象实例的类型,匹配 AOP 被代理对象的类型。

表达式模式:

target(declaring-type)

使用示例

// 匹配目标对象类型为service包下的类
target(com.xyz.service.*)

// 匹配目标对象类型为service包及其子包下的类
target(com.xyz.service..*)

// 匹配目标对象类型为AccountServiceImpl的类
target(com.xyz.service.AccountServiceImpl)

三种表达式匹配范围如下:

表达式匹配范围withinthistarget
接口
实现接口的类
不实现接口的类

5.args

匹配方法参数类型和数量,参数类型可以为指定类型及其子类。

使用 execution 表达式匹配参数时,不能匹配参数类型为子类的方法。

表达式模式:

args(param-pattern)

使用示例 

// 匹配参数只有一个且为Serializable类型(或实现Serializable接口的类)
args(java.io.Serializable)

// 匹配参数个数至少有一个且为第一个为Example类型(或实现Example接口的类)
args(cn.codeartist.spring.aop.pointcut.Example,..)

6.bean

通过 bean 的 id 或名称匹配,支持 * 通配符。

bean(bean-name)

使用示例

// 匹配名称以Service结尾的bean
bean(*Service)

// 匹配名称为demoServiceImpl的bean
bean(demoServiceImpl)

7.@within

匹配指定类型是否含有注解。当定义类时使用了注解,该类的方法会被匹配,但在接口上使用注解不匹配。

使用示例:

// 匹配使用了Demo注解的类
@within(cn.codeartist.spring.aop.pointcut.Demo)

8.@target

匹配目标对象实例的类型是否含有注解。当运行时对象实例的类型使用了注解,该类的方法会被匹配,在接口上使用注解不匹配。

使用示例:

// 匹配对象实例使用了Demo注解的类
@target(cn.codeartist.spring.aop.pointcut.Demo)

9.@annotation

匹配方法是否含有注解。当方法上使用了注解,该方法会被匹配,在接口方法上使用注解不匹配。

使用示例:

// 匹配使用了Demo注解的方法
@annotation(cn.codeartist.spring.aop.pointcut.Demo)

10.@args

匹配方法参数类型是否含有注解。当方法的参数类型上使用了注解,该方法会被匹配。

使用示例:

// 匹配参数只有一个且参数类使用了Demo注解
@args(cn.codeartist.spring.aop.pointcut.Demo)

// 匹配参数个数至少有一个且为第一个参数类使用了Demo注解
@args(cn.codeartist.spring.aop.pointcut.Demo,..)

切点表达式的参数匹配

切点表达式中的参数类型,可以和通知方法的参数通过名称绑定,表达式中不需要写类或注解的全路径,而且能直接获取到切面拦截的参数或注解信息。

@Before("pointcut() && args(name,..)")
public void doBefore(String name) {
    // 切点表达式增加参数匹配,可以获取到name的信息
}

@Before("@annotation(demo)")
public void doBefore(Demo demo) {
    // 这里可以直接获取到Demo注解的信息
}

切点表达式的参数匹配同样适用于 @within, @target, @args

怎样编写一个好的切点表达式?

要使切点的匹配性能达到最佳,编写表达式时,应该尽可能缩小匹配范围,切点表达式分为三大类:

类型表达式:匹配某个特定切入点,如 execution
作用域表达式:匹配某组切入点,如 within
上下文表达式:基于上下文匹配某些切入点,如 this、target 和 @annotation


一个好的切点表达式应该至少包含前两种(类型和作用域)类型。
作用域表达式匹配的性能非常快,所以表达式中尽可能使用作用域类型。
上下文表达式可以基于切入点上下文匹配或在通知中绑定上下文。
单独使用类型表达式或上下文表达式比较消耗性能(时间或内存使用)。

三、切点表达式组合

使用 &&|| 和 ! 来组合多个切点表达式,表示多个表达式“与”、“或”和“非”的逻辑关系。
这可以用来组合多种类型的表达式,来提升匹配效率。

// 匹配doExecution()切点表达式并且参数第一个为Account类型的方法
@Before("doExecution() && args(account,..)")
public void validateAccount(Account account) {
    // 自定义逻辑
}

1. 常用注解
@Pointcut    指定切点表达式


2. 切点表达式类型
execution          匹配方法切入点
within                匹配指定类型
this                    匹配代理对象实例的类型
target                匹配目标对象实例的类型
args                  匹配方法参数
bean                 匹配 bean 的 id 或名称
@within            匹配类型是否含有注解
@target            匹配目标对象实例的类型是否含有注解
@annotation    匹配方法是否含有注解
@args              匹配方法参数类型是否含有注解

原文链接:https://blog.csdn.net/weixin_43793874/article/details/124753521

6.AOP的通知顺序

1.默认按照切面类的字母排序执行,字母排序越小的前置通知先执行,后置通知后执行,反之易然。

2.若需要自定以执行顺序,可以如下图使用@order()注解控制切面类的执行顺序,@order()注解的值越小,前置通知越先执行,后置通知越后执行

7.AOP的使用案例(使用@annotation的切点表达式)

1.导入AOP依赖

<!-- 切面依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

2.首先需要新建一个注解用于标记目标的方法

package com.test.aspect.annotation;

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

/**
 * @Details 自定义日志记录
 */
@Target(ElementType.METHOD)//目标对象:方法类型
@Retention(RetentionPolicy.RUNTIME)//执行时间,目标方法运行时执行
public @interface WebLog {
    //日志级别
    String desc() default "info";
    //日志信息
    String message();

}

3.编写切面类

package com.test.aspect;

import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
import com.hangyi.aspect.model.WebLog;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.*;

/**
 * @Details: @MyLog 注解切面类
 */
@Component
@Aspect
@Slf4j
public class WebLogAspect {

    @Pointcut("@annotation(com.hangyi.aspect.annotation.WebLog)")
    private void webLog(){}

    @Around("webLog()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
      //开始时间
        Long starTime = System.currentTimeMillis();
       Object result = joinPoint.proceed();
        //结束时间
        long endTime= System.currentTimeMillis();
        System.out.print("方法运行耗时:"+endTime-starTime);
}

4.在需要记录方法耗时的方法中加入步骤一新增的注解

package com.test.controller;

import com.hangyi.aspect.annotation.WebLog;
import com.hangyi.test.CommonResult;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;


@RestController
@RequestMapping("/test")
public class TestController {

    @WebLog(message = "健康测试")
    @GetMapping("/healthTest")
    @ApiOperation(value="健康测试")
    public CommonResult<String> getTest(){
        return CommonResult.success("healthTest");
    }
}

此时该运行该方法时会运行对应的切面类,记录方法运行时间。

  • 22
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值