Spring AOP认识

一、定义

AOP即面向切面编程,是一种设计思想,旨在把多个互不关系的类的非核心公共行为和核心行为进行分离。AOP是面向对象编程(OOP)的补充,传统的面向对象编程,若多个互不关系的类存在相同的行为,比如日志记录、权限校验等,开发者需要在这些类中编写许多重复的代码,造成大量的代码冗余,可复用性和可维护性较差,而AOP对OOP的补充,通过切点、通知、织入这些功能把这些公共行为和核心行为进行分离,从而大大提供代码的可复用性和可维护性。

Spring AOP是Spring的AOP实现,它是Spring的核心特性之一,它借用了AspectJ的一组注解,自己基于这些注解通过动态代理实现了AOP,使开发者能够轻松地将关注点集成到他们的应用程序中。

二、核心概念

1、AOP通用概念

(1)切面(Aspect)

切面是封装横切关注点的代码模块由切入点和通知组成。在Spring AOP中,一个切面可以定义在什么时候、什么地方以及如何应用某种特定的行为到目标对象上。

  • 切点(Pointcut)

    • 连接点(Joinpoint)

切点用于定义横切关注点应用的时机和范围。它是一组连接点的集合,这些连接点共享相同的特性。切点的切点表达式execution()用于匹配连接点,从而确定哪些连接点应该接收通知。

切点表达式的认识

a、表达式的格式

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)
具体解释:

  • modifiers-pattern: 方法的访问修饰符,如 public、protected 等,可以省略
  • ret-type-pattern: 方法的返回类型,如 void、int 等。
  • declaring-type-pattern: 方法所属的类的类型模式,可以使用通配符 * 匹配任意字符。
  • name-pattern: 方法的名称模式,可以使用通配符 * 匹配任意字符。
  • param-pattern: 方法的参数模式,包括参数类型和个数。
  • throws-pattern: 方法抛出的异常类型。

b、举例

  • 所有公共方法的执行

        execution(public * *(..))

  • 名称以 set 开头的所有方法的执行

        execution(* set*(..))

  • AccountService 接口定义的任何方法的执行

        execution(* com.xyz.service.AccountService.*(..))

  • service 包中定义的任何方法的执行

        execution(* com.xyz.service.*.*(..))

连接点是代表应用程序执行过程中可以插入切面(Aspect)的地方,例如方法的调用、字段的访问等。在Spring AOP中,一个连接点总是代表一个方法的执行。连接点是AOP框架可以在其上 “织入” 切面的点。

  • 通知(Advice)

通知定义了在切入点匹配时要执行的代码,它是增强应用到连接点上的行为。通知有多种类型,包括前置通知(Before Advice)、后置通知(After Advice) 、环绕通知(Around Advice) 、异常通知(After Throwing Advice) 和  返回通知(After Returning Advice)  。这些通知类型决定了增强在连接点上的执行顺序和方式。

通知的执行顺序:

a、无异常存在的执行顺序

环绕前置--> 普通前置--> 目标方法执行--> 普通返回 --> 普通后置--> 环绕返回 -->环绕后置

b、有异常存在的执行顺序

环绕前置--> 普通前置 --> 目标方法执行 -->普通异常 --> 普通后置
注意:异常通知与返回通知是互斥的,有异常无返回,有返回无异常

(2)目标对象

被一个或多个切面所通知的对象。也被称为被通知(advised)对象。由于Spring AOP是通过代理模式实现的,因此在运行时,目标对象总是用代理对象所包裹,所以目标对象也是被代理对象

(3)织入(Weaving)

织入是将切面应用到目标对象并创建代理对象的过程。这是AOP框架在运行时或编译时完成的核心任务。

2、SpringAOP额外新增的概念

Advisor

Advisor包含一个切入点和通知,而Aspect虽然也可以包含切入点和通知,但是可以包含多个切入点和通知。

686a2f0c464346858a7dc6377873b5c6.png

三、使用

下面两种方式都需要添加@EnableAspectJAutoProxy注解

1、通过AspectJ注解

(1)切面代码

@Component
@EnableAspectJAutoProxy
@Aspect
public class AspectTest {
    @Pointcut("execution(* org.example.aop.*.*(..))")
    public void pointcut(){}

    @Before("pointcut()")
    public void before(){
        System.out.println("方法执行前");
    }

    @After("pointcut()")
    public void after(){
        System.out.println("方法执行后");
    }

    @AfterReturning("pointcut()")
    public void afterReturning(){
        System.out.println("方法返回后通知");
    }

    @AfterThrowing("pointcut()")
    public void afterThrowing(){
        // 异常通知与返回通知是互斥的,有异常无返回,有返回无异常
        System.out.println("方法异常后通知");
    }

    @Around("pointcut()")
    public void around(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("方法环绕通知前");
        // 必须下面这行代码,不然除了环绕通知外的通知无法生效
        joinPoint.proceed();
        System.out.println("方法环绕通知后");
    }
}

(2)目标对象代码

@Component
public class TestAopMethod {
    public void test(){
        System.out.println("I AM TEST METHOD");
    }
}

(3)测试代码

    @Autowired
    TestAopMethod testAopMethod;

    @Override
    public void run(String... args) throws Exception {
        testAopMethod.test();
    }

执行结果:

e9bdede289cb4a49ae98902c65fccf84.png

2、通过Advisor

(1)编写切入点Pointcut

public class MyPointCut implements Pointcut {
    @Override
    public ClassFilter getClassFilter() {
        // 判断类是否匹配
        return new MyClassFilter();
    }

    @Override
    public MethodMatcher getMethodMatcher() {
        // 判断方法是否匹配
        return new MyMethodMatcher();
    }

    private class MyMethodMatcher implements MethodMatcher{

        @Override
        public boolean matches(Method method, Class<?> targetClass) {
            Annotation[] annotations=method.getDeclaredAnnotations();
            if(annotations==null||annotations.length==0)return false;

            for(Annotation annotation:annotations){
                if(annotation.annotationType()== MyAnnotation.class){
                    return true;
                }
            }

            return false;
        }

        @Override
        public boolean isRuntime() {
            return false;
        }

        @Override
        public boolean matches(Method method, Class<?> targetClass, Object... args) {
            return false;
        }
    }

    private class MyClassFilter implements ClassFilter{
        @Override
        public boolean matches(Class<?> clazz) {
            return AnnotationUtils.isCandidateClass(clazz,MyAnnotation.class);
        }
    }
}

(2)编写通知Advice

public class MyInterceptAdvice implements MethodInterceptor {
    @Nullable
    @Override
    public Object invoke(@Nonnull MethodInvocation invocation) throws Throwable {
        System.out.println("MyInterceptAdvice invoke before");
        return invocation.proceed();
    }
}

(3)编写Advisor

@EnableAspectJAutoProxy
@Component
public class MyAdvisor implements PointcutAdvisor {
    @Override
    public Advice getAdvice() {
        // 通知
        return new MyInterceptAdvice();
    }

    @Override
    public boolean isPerInstance() {
        return false;
    }

    @Override
    public Pointcut getPointcut() {
        // 切入点
        return new MyPointCut();
    }
}

(4)编写目标类

public class TestAopMethod {
    @MyAnnotation
    public void test(){
        System.out.println("I AM TEST METHOD");
    }
}

(5)编写测试代码

    public static void main(String[] args) {
        AnnotationConfigApplicationContext context1=new AnnotationConfigApplicationContext(SysConfig.class);
        TestAopMethod obj= (TestAopMethod) context1.getBean("testAopMethod");
        obj.test();
    }

执行结果:

cfff11a282634e25af4c0ee87bd2e742.png

四、原理

1、认识

2、详细过程调用

(1)前置环境:

有一个SysConfig类,添加了@ComponentScan(basePackages = "org.example.aop")和@Configuration注解。该配置类的作用是通过@ComponentScan注解扫描basePackages 包下所有声明了@Component注解的bean信息,比如切面类AspectTest
的bean信息,以及也通过@Bean配置目标类TestAopMethod的bean信息。而切面类AspectTest添加了@EnableAspectJAutoProxy注解和@Component注解。有一个main方法,方法内容是创建一个AnnotationConfigApplicationContext容器,并通过该容器的getBean方法获取TestAopMethod实例,最后调用该实例的test方法(该test方法就是切点,程序会在该方法前后进行增强)

(2)大体流程

启动程序,创建容器AnnotationConfigApplicationContext并初始化。在这个过程中,会先添加ConfigurationClassPostProcessor和入口配置类SysConfig的bean信息到容器的beanDefinitionMap中。接着会进行容器刷新。在这个刷新过程,有两个主要的方法一个是invokeBeanFactoryPostProcessors方法,该方法会先找到ConfigurationClassPostProcessor的bean信息,然后调用该BeanFactoryPostProcessor的processConfigBeanDefinitions方法对入口配置类SysConfig进行解析,包括对@ComponentScan、@Import等常见注解解析,而启动Spring AOP功能的注解@EnableAspectJAutoProxy就是在此处解析,该注解真正解析的是@Import注解,里面配置了一个AspectJAutoProxyRegistrar类,会在解析之后,调用loadBeanDefinitionsFromRegistrars方法来执行AspectJAutoProxyRegistrar类的registerBeanDefinitions方法,会在这个registerBeanDefinitions里面向容器添加AnnotationAwareAspectJAutoProxyCreator的bean信息。另一个方法是finishBeanFactoryInitialization,该方法会对BeanDefinitionMap中非懒加载的bean信息通过调用容器的getBean方法进行实例化。这里主要介绍TestAopMethod的实例化,在getBean方法里面会对创建并已初始化好的TestAopMethod实例,调用初始化后applyBeanPostProcessorsAfterInitialization方法来为该实例创建代理对象,applyBeanPostProcessorsAfterInitialization方法会去调用AnnotationAwareAspectJAutoProxyCreator这个BeanProcessor的postProcessAfterInitialization方法,但是AnnotationAwareAspectJAutoProxyCreator未重写该方法,所以真正调用的是它的祖父类AbstractAutoProxyCreator的postProcessAfterInitialization方法,该方法的核心是使用wrapIfNecessary对目标进行包装。wrapIfNecessary方法会先通过getAdvicesAndAdvisorsForBean找到符合条件的切面类(声明了@Aspect注解)和Advisor接口实现类,该getAdvicesAndAdvisorsForBean的具体过程是先通过findCandidateAdvisors找到所有声明了@Aspect注解的切面类和Advisor接口实现类,然后通过findAdvisorsThatCanApply根据TestAopMethod的类名找到符合条件的切面类和Advisor接口实现类。接着,基于这些符合条件的类利用createProxy创建代理对象。这里会把TestAopMethod实例作为TargetSource参数传入createProxy方法,接着会在该方法内根据TargetSource创建ProxyFactory对象,调用ProxyFactory的getProxy方法获取代理对象,该getProxy方法会先根据TargetSource是否有实现接口来判断是使用JDK动态代理或者CGLIB动态代理方式创建对应的AopProxyFactory,之后使用AopProxyFactory来创建代理对象。最后把这个TestAopMethod实例放入一级缓存。

生成的的代理对象

c3d42b387b8a414fae9b1fd3b0ab2b9c.png

五、问题

1、在某个项目中,有个包aop,在这个包下有3个文件,一个是切面类AspectTest,一个是配置类SysConfig,一个是目标类TestAopMethod。该配置类里通过@ComponentScan扫描AspectTest,以及通过@Bean注解声明TestAopMethod的bean信息。之后利用收到创建容器AnnotationConfigApplicationContext的方法获取TestAopMethod实例,并调用它的方法test,会出现TestAopMethod获取不到(即为空)的错误,若把SysConfig重新移入另一个包config,会发现不会报错,原因是为什么呢?

2e2c44b220dd443691b21b6ec02acb4a.png25b25e4b47354e508b249439c1fb102b.png

  • 14
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值