Spring-AOP

文章目录

Spring AOP

一、AOP 概述

1、概述

AOP 全称为 Aspect Oriented Programming,即 面向切面编程,是一种编程范式。

AOP 用于将横切关注点(cross-cutting concerns)从核心业务逻辑中分离出来,以提高代码的模块化、可维护性和重用性。横切关注点是指那些在应用程序中存在于多个模块和层次中的功能,如日志记录、事务管理、安全性、缓存、性能监控等。

Spring 框架提供了强大的 AOP 支持,通过 Spring AOP,可以很方便地实现横切关注点的模块化。

2、原理

Spring AOP 采用了代理模式实现,通过动态代理或者字节码生成技术来在运行时为目标对象创建代理对象,并在代理对象的方法调用前后执行通知。实现在【不修改java源代码】的情况下,【运行时】实现方法功能的【增强】

代理模式:

  • 静态代理:程序编译运行前由程序员或工具手动创建。

  • 动态代理:在运行时动态生成代理类,用于代理目标对象的方法调用。

JDK 动态代理和 CGLIB 动态代理是两种常见的 Java 动态代理实现方式:

  • Jdk动态代理:
    • Java 标准库提供的一种动态代理实现方式,基于 Java 反射机制。
    • 要求目标对象必须实现一个或多个接口,代理对象和目标对象实现了相同的接口。
  • Cglib动态代理:
    • 在运行时动态生成目标对象的子类作为代理类,而不需要目标对象实现接口。

Jdk动态代理适用于对接口进行代理,Cglib动态代理适用于对进行代理。

3、相关术语

AOP 的核心思想是通过切面(Aspect)将横切关注点模块化,并将其应用到程序的不同部分。

切面(Aspect)由切点(Pointcut)和通知(Advice)组成,

  • 其中切点定义了在何处应用通知,而通知定义了在切点处执行的操作。
markdown 复制代码
# Joinpoint(连接点):
	待增强功能的方法。(动态,一个动作)

# Pointcut(切入点):
	要拦截的Joinpoint。(静态,一个点)

# Advice(通知):
	拦截到Joinpoint之后要做的事情,即增强的功能
	通知类型:前置通知、后置通知、异常通知、最终通知、环绕通知

# Target(目标对象):
	被代理的对象。

# Proxy(代理对象):
	代理类对象(一个类被AOP织入增强后产生)

# Weaving(织入):
	增强目标对象,创建代理对象的过程

# Aspect(切面):
	切入点 + 通知

4、五种通知类型

markdown 复制代码
# 通知类型
1. 前置通知 @Before
		在目标方法 执行前 执行
2. 后置通知 @AfterReturning
		在目标方法 正常返回后 执行。(它和异常通知只能执行一个)
3. 异常通知 @AfterThrowing
		在目标方法 发生异常后 执行。(它和后置通知只能执行一个)
4. 最终通知 @After
		无论目标方法正常返回,还是发生异常都会执行
5. 环绕通知 @Around
		在目标方法 执行前后 执行该增强方法。(一定要抛出异常,才能回滚事务)

# 执行顺序
1. 正常
		@Before -> @Around -> Method -> @Around -> @After -> @AfterReturning
2. 异常
		@Before -> @Around -> Method -> @Around -> @After -> @AfterThrowing

其中需要注意:使用@Around时,一定要将异常抛出,否则事务不会回滚。

二、手写简易AOP

步骤分析:

  • 创建两个代理类,增强被代理对象的功能
  • 创建代理类工厂,获取代理类对象
  • 初始化IOC时,使用动态代理加入切面AOP

0、被代理类/接口

java 复制代码
public class AopClass {
    public UserEntity test() {
        return UserEntity.builder().id(1L).username("测试name").build();
    }
}
java 复制代码
public interface AopInterface {
    UserEntity test();
}
java 复制代码
public class AopInterfaceImpl implements AopInterface {
    @Override
    public UserEntity test() {
        return UserEntity.builder().id(1L).username("测试name").build();
    }
}

1、Jdk代理类

java 复制代码
/**
 * Jdk动态代理
 */
public class JdkProxy implements InvocationHandler {

    /**
     * 被代理的对象(Target 目标对象)
     */
    private Object targetObject;

    /**
     * 需要被代理的方法(JoinPoint 连接点)
     */
    public String joinPoint = "test";

    /**
     * 初始化 被代理的对象
     */
    public JdkProxy(Object targetObject) {
        this.targetObject = targetObject;
    }

    /**
     * Object proxy:代理类对象
     * Method method:执行的目标方法
     * Object[] args:方法参数
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 执行的目标方法名称
        String name = method.getName();
        // 判断是否需要代理(PointCut 切入点)
        if (joinPoint.equals(name)) {
            // 前置增强(Advice 通知)
            System.out.println("执行了JDK代理");
        }
        // 具体执行目标方法
        return method.invoke(targetObject, args);
    }

}

2、Cglib代理类

java 复制代码
/**
 * Cglib动态代理
 */
public class CglibProxy implements MethodInterceptor {

    /**
     * 被代理的对象(Target 目标对象)
     */
    private Object targetObject;
    
    /**
     * 要代理的目标方法(JoinPoint 连接点)
     */
    public String joinPoint = "test";

    /**
     * 初始化 被代理的对象
     */
    public CglibProxy(Object targetObject) {
        this.targetObject = targetObject;
    }

    /**
     * Object o:代理类对象
     * Method method:执行的目标方法
     * Object[] objects:方法参数
     * MethodProxy methodProxy:方法的代理
     */
    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        // 执行的目标方法名称
        String name = method.getName();
        // 判断是否需要代理(PointCut 切入点)
        if (joinPoint.equals(name)){
            // 前置增强(Advice 通知)
            System.out.println("执行了Cglib代理");
        }
        // 具体执行目标方法
        // return methodProxy.invokeSuper(o,objects);
        return method.invoke(targetObject, objects);
    }

}

3、代理类工厂

java 复制代码
/**
 * 代理类工厂
 */
public class ProxyBeanFactory {

    /**
     * 被代理的对象
     */
    private Object targetObject;

    /**
     * 初始化 被代理的对象
     */
    public ProxyBeanFactory(Object targetObject) {
        this.targetObject = targetObject;
    }

    /**
     * Weaving(织面):实现增强,获取代理对象 的过程
     */
    public Object getProxyBean() {
        // 代理对象(Proxy 代理对象)
        Object proxyObject;
        // 反射获取 被代理对象 实现的接口
        Class<?>[] interfaces = targetObject.getClass().getInterfaces();
        // 判断是否实现接口
        if (interfaces.length > 0) {
            // 实现了接口,使用jdk代理
            proxyObject = Proxy.newProxyInstance(
                targetObject.getClass().getClassLoader(),
                interfaces,
                new JdkProxy(targetObject)
            );
        } else {
            // 没有实现接口,使用cglib代理
            proxyObject = Enhancer.create(
                targetObject.getClass(),
                new CglibProxy(targetObject)
            );
        }
        // 返回代理对象
        return proxyObject;
    }
}

4、BeanFactory

java 复制代码
/**
 * Bean工厂-将对象放入IOC容器,交由Spring管理
 * 1、如果没有AOP,则放入IOC容器的是普通对象
 * 2、如果存在AOP,则放入IOC容器的是代理对象
 */
@Component
public class BeanFactory {

    /**
     * ioc容器
     */
    private static final HashMap<String, Object> IOC = new HashMap<>();

    static {
        try {
            // 需要存到IOC容器的bean
            Map<String, String> beanMap = Maps.newHashMap();
            beanMap.put("aopInterface", AopInterfaceImpl.class.getName());
            beanMap.put("aopClass", AopClass.class.getName());

            for (Entry<String, String> entry : beanMap.entrySet()) {
                String beanName = entry.getKey();
                String className = entry.getValue();

                // 1、反射获取 被代理对象
                Object object = Class.forName(className).newInstance();
                // 2、被代理对象 传入 代理工厂
                ProxyBeanFactory proxyBeanFactory = new ProxyBeanFactory(object);
                // 3、代理工厂 获取 代理对象
                Object proxyBean = proxyBeanFactory.getProxyBean();
                // 4、代理对象 存入IOC容器
                IOC.put(beanName, proxyBean);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 从容器中获取对象
     */
    public static Object getBean(String className) {
        return IOC.get(className);
    }

}

5、测试类

java 复制代码
@Test
public void aopDemo() {
    AopInterface aopInterface = (AopInterface) BeanFactory.getBean("aopInterface");
    AopClass aopClass = (AopClass) BeanFactory.getBean("aopClass");
    System.out.println(aopInterface.test());
    System.out.println(aopClass.test());
}
java 复制代码
执行了JDK代理
UserEntity(id=1, username=测试name, birthday=null, sex=null, address=null, email=null)
执行了Cglib代理
UserEntity(id=1, username=测试name, birthday=null, sex=null, address=null, email=null)

三、JoinPoint

1、概述

JoinPoint 主要用于在通知(Advice)中获取被拦截方法的信息,并在方法执行前后执行相应的逻辑。

通过 JoinPoint,我们可以获取被拦截方法的参数、目标对象、方法签名等信息,从而实现各种横切关注点的功能。

java 复制代码
public interface JoinPoint {
    // 获取`Signature`对象,封装了目标方法名、Class等信息
    Signature getSignature();
    
    // 获取传入目标方法的参数对象
    Object[] getArgs();
    
    // 获取被代理的对象
    Object getTarget();
    
    // 获取代理对象
    Object getThis();
}

Signature 接口的子接口 MethodSignature 提供了获取目标方法的方法

java 复制代码
public interface MethodSignature extends CodeSignature {
    Class getReturnType();

    // 获取目标方法
    Method getMethod();
}

Spring AOP 中的通知方法可以接受 JoinPoint 类型的参数,从而可以在通知中访问被拦截方法的信息。

2、使用案例

将自定义的 JoinPointTest 注解加在目标方法即可实现增强

java 复制代码
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface JoinPointTest {
    String actionName() default "默认操作";
}
java 复制代码
@Data
@Builder
public class JoinPointParam {
    private Long id;
    private String username;
}
java 复制代码
@Component
@Aspect
@Slf4j
public class JoinPointAspect {
    /**
     * 切点表达式中的joinPointTest引用的是方法参数中的joinPointTest
     */
    @Around("@annotation(joinPointTest)")
    public Object aroundAspect(ProceedingJoinPoint joinPoint, JoinPointTest joinPointTest) {
        log.warn("JoinPointAspect开始");

        StopWatch sw = new StopWatch();
        sw.start();

        // 请求参数
        Object[] args = joinPoint.getArgs();
        String argsJson = JsonUtils.objectToJson(args);
        log.warn("获取请求参数:{}", argsJson);

        // 方法信息
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        log.warn("获取方法信息:{}", method);

        // 注解信息
        Transactional annotation = method.getAnnotation(Transactional.class);
        log.warn("获取注解信息:{}", annotation);

        // 切面注解
        String actionName = joinPointTest.actionName();
        log.warn("切面注解actionName属性:{}", actionName);

        // declaringClass
        Class<?> declaringClass = method.getDeclaringClass();
        log.warn("获取declaringClass:{}", declaringClass);

        // 执行目标方法
        Object result;
        try {
            // 执行目标方法
            result = joinPoint.proceed();

            sw.split();
            log.warn("目标方法 {} 执行结束,返回值:{},耗时:{}", 
                     method.getName(), result, sw.getSplitTime());

            return result;
        } catch (Throwable throwable) {
            // 系统异常 --> 执行异常逻辑
            log.error("切面方法执行异常 ---> 企业微信告警");
            // 抛出异常 --> 为了事务回滚
            throw new RuntimeException(throwable);
        } finally {
            log.warn("JoinPointAspect结束,耗时:{}", sw.getTime());
            sw.stop();
        }

    }

}

四、切面的执行顺序

1、@Order

使用@Order 注解直接定义切面顺序

java 复制代码
// 值越小优先级越高
@Order(3)
@Component
@Aspect
public class LoggingAspect implements Ordered {...}

2、实现Ordered接口

java 复制代码
@Component
@Aspect
public class LoggingAspect implements Ordered {

    // ....

    @Override
    public int getOrder() {
        // 返回值越小优先级越高
        return 1;
    }
}

五、切点表达式

切点表达式 用于找到需要被切入的连接点

1、切点表达式语法

复制代码
[修饰符] 返回值类型 包名.类名.方法名(参数)
java 复制代码
// 全匹配方式
	public 
    java.util.List 
    com.xxx.spring.service.impl.AccountServiceImpl.findById(Long)
// 省略访问修饰符
	java.util.List 
    com.xxx.spring.service.impl.AccountServiceImpl.findById(Long)
// 返回值使用*号,表示任意返回值
	* com.xxx.spring.service.impl.AccountServiceImpl.findById(Long)
// 包名使用*号,表示任意包(但是有几级包,就需要写几个*)
	* *.*.*.*.*.AccountServiceImpl.queryAll()
// 使用..表示当前包及其子包
	* *..AccountServiceImpl.queryAll()
// 类名使用*号,表示任意类
	* *..*.queryAll()
// 方法名使用*号,表示任意方法
	* *..*.*()
// 参数列表使用 * 表示参数可以是任意数据类型,但是必须有参数
	* *..*.*(*)
// 参数列表使用 .. 表示有无参数均可,有参数可以是任意类型
	* *..*.*(..)
// 全通配方式:不推荐
	* *..*.*(..)

2、使用方式

方式1:直接作为 通知注解 的 value 属性

java 复制代码
@Around("@within(org.springframework.stereotype.Service)")
public Object userActionLogMethod(ProceedingJoinPoint joinPoint) throws Throwable {
    ...
}

方式2:作为 @Pointcut 注解的 value 属性,用以定义连接点

java 复制代码
@Pointcut("within(@org.springframework.stereotype.Repository *)")
public void aroundMethod() {}

@Around("aroundMethod()")
public Object aroundLogInfo(ProceedingJoinPoint joinPoint) throws Throwable {
    ...
}
  • 将切点表达式"within(@org.springframework.stereotype.Repository *)"写到@Pointcutvalue

  • @Pointcut修饰的aroundMethod()AOP AspectJ 定义为切点签名方法

    作用是使得 通知注解 可以通过这个切点签名方法连接到切点,然后通过解释 切点表达式 找到需要被切入的连接点。

3、常用的切点表达式

通常情况下,我们都是对业务层的方法进行增强,所以切入点表达式都是切到业务层实现类

java 复制代码
@Pointcut("execution(* com.xxx.app.service.impl.*.*(..))");

execution :匹配 方法签名(返回类型,包名,类名,方法,参数是必须的)

java 复制代码
execution(public * *(..))				// 匹配所有public方法
execution(* set*(..))	  				// 匹配所有方法名以set开头的方法
execution(* com.xyz.service.*.*(..))	// 匹配service包下的所有方法
execution(* com.xyz..*.*(..))			// 匹配xyz包及其子包下的所有方法  

execution(* *..find*(Long))		  // 匹配所有以find开头 并且 只一个Long类型参数 的方法
execution(* *..find*(Long,..))    // 匹配一个有任意个参数 并且 第一个参数必须是Long类型的方法
    
execution(* com.xyz.service.AccountService.*(..))	// 匹配AccountService类下的所有方法 

within:匹配 某个确定类型的类 / 某个包下的方法

java 复制代码
within(org.baeldung.dao.FooDao)		// 某个确定的类
within(com.xyz.service.*)			// 匹配service包下的所有方法
within(com.xyz.service..*)			// 匹配service包或其子包下的所有方法

bean:匹配 指定的bean

java 复制代码
bean(tradeService)		// 匹配命名为tradeService的类的方法
bean(*Service)			// 匹配命名后缀为Service的类的方法

target:pointcut 所选取的 Join point 的所有者,直白点说就是: 指明拦截的方法属于哪个基类。

this:pointcut 所选取的 Join point 的调用的所有者,就是说:方法是在那个类中被调用的。

java 复制代码
// 匹配所有实现了AccountService接口的代理对象(注意是代理类)    
this(com.xyz.service.AccountService)
// 匹配所有实现了AccountService接口的bean(注意是本类)    
target(com.xyz.service.AccountService)
    
// 匹配基于JDK动态代理的代理类(当前要代理的类对象实现了某个接口)
target(org.baeldung.dao.BarDao)
// 匹配基于CGLIB的代理类(当前要代理的类对象没有实现某个接口)
this(org.baeldung.dao.FooDao)   

args

java 复制代码
// 匹配只有一个入参,且入参实现了Serializable接口的方法    
args(java.io.Serializable)

注解相关

java 复制代码
// 匹配所有标注了@Transactional注解的连接点 
@annotation(org.springframework.transaction.annotation.Transactional)

// 匹配指定连接点,这个连接点所属的目标对象的类标注了@Transactional注解
@target(org.springframework.transaction.annotation.Transactional)

// 匹配标注了@Transactional注解的类中的所有连接点
@within(org.springframework.transaction.annotation.Transactional)

// 匹配只有一个入参,且运行时入参有@Classified注解的方法    
@args(com.xyz.security.Classified)

可以使用 &&、||、! 三种运算符来组合切点表达式,表示与或非的关系。

java 复制代码
@Pointcut("@target(org.springframework.stereotype.Repository)")
public void repositoryMethods() {}
 
@Pointcut("execution(* *..create*(Long,..))")
public void firstLongParamMethods() {}
 
@Pointcut("repositoryMethods() && firstLongParamMethods()")
public void entityCreationMethods() {}
相关推荐
Rust研习社13 小时前
组合真的优于继承吗?为什么 Rust 和 Go 都拥抱组合舍弃继承?
后端·rust·编程语言
IT_陈寒13 小时前
JavaScript的闭包把我坑惨了,说好的内存会自动回收呢?
前端·人工智能·后端
CaffeinePro14 小时前
Pydantic深度使用:数据校验、枚举、ORM映射
后端·fastapi
Chenyiax15 小时前
从 Chat 到 Responses:OpenAI API 抽象为什么变了?
后端
MariaH15 小时前
Koa和Express的区别
后端
MariaH15 小时前
Koa框架的使用
后端
luckdewei16 小时前
那个用 passlib 做认证的新同事,上线第一天就把用户密码写进了日志
后端
ping某17 小时前
为什么 Nginx 明明监听了 80,转发后端时却用了 4xxxx 端口?
后端·nginx
JustHappy17 小时前
我汇总了身边朋友的经历才发现,其实第一份实习是最难找的......
前端·后端·面试
uhakadotcom17 小时前
在python 的 工程化架构中 ,什么是 薄包装器层?
后端·面试·github