AOP小记

编程思想演进:POP(Procedure Oriented Programming)-> OOP(Object Oriented Programming)-> AOP(Aspect Oriented Programming)

三者不是孤立的,往往互相配合使用。

 

1. AOP(Aspect Oriented Programming)

AOP即面向切面编程,一种编程思想。面向对象编程以纵向的编程方式对业务逻辑进行拆分,而面向切面编程则是以横向的方式将与核心业务逻辑关联小的业务独立出来并以很小的侵入性代价与核心业务整合,常用于性能检测、权限验证、日志记录、事务控制、异常处理等。示意:

 

2. 主要概念

AOP规范的框架实现主要有Spring AOP(JDK动态代理、CGLIB动态代理)、AspectJ,两者都遵循AOP规范且术语基本一致(Spring 2.0后使用了与AspectJ一样的注解)。

  • target:被拦截的目标对象
  • proxy:被拦截的对象经AOP处理后产生新的对象,为代理对象
  • joinpoint(连接点):指定哪些目标函数可以被拦截
  • pointcut(切入点):指定对哪些jointpoint进行拦截
  • advice(通知/增强):指定在某些特定的pointcut上需要执行的动作(代码),如进行日志记录。通知的执行时机有:
  1. Before:前置通知。在目标函数执行前执行通知
  2. After:后置通知。在目标函数执行后执行通知,无论目标函数是否正常执行都会执行此通知
  3. AfterReturning:后置返回通知。在目标函数返回时执行通知
  4. AfterThrowing:异常通知。在目标函数抛出异常时执行通知
  5. Around:环绕通知。在目标函数执行时执行,可控制目标函数是否执行
  • aspect(切面):由pointcut和advice相结合而成,定义adcice应用到哪些pointcut上
  • ewaving(织入):把切面代码应用(织入)到被拦截的对象的目标函数过程,通常是通过创建代理对象实现的。Spring AOP是通过实现后置处理器BeanPostProcessor接口来实现织入的。也就是在bean完成初始化之后,通过给目标对象生成代理对象,并交由springIOC容器来接管,这样再去容器中获取到的目标对象就是已经增强过的代理对象。

总结而言:joinpoint、pointcut 定义在哪里做,advice 定义做什么,ewaving定义内部怎么做。开发者在使用时只需要指定前两者,第三者由框架实现。

 

3. 框架实现

原理

AOP的框架实现主要有Spring AOP、AspectJ AOP,两者都遵循AOP规范且术语基本一致。区别:

  • 前者动态织入(运行时动态生成代码应用到目标类):采用JDK动态代理、CGLIB代理等实现。功能没有后者全面(对大多数项目来说够用了),侧重于与Spring IOC整合。
  • 后者静态织入(在编译期将aspec代码编译织入到目标类):相当于静态代理。功能更全,但需要AspectJ特殊的编译器配合。(题外话:Python Numpy、JPython、AspectJ、IronPython作者均为美帝大牛Jim Hugunin,具体可参阅 https://mp.weixin.qq.com/s/0lpsJgZkuFMOu-fsrGeY3Q

Spring 2.0后使用了与AspectJ一样的注解,但底层仍是动态代理技术的实现,而没有依赖于 AspectJ 的编译器。

Spring AOP基于动态代理实现,原理图:

 AspectJ基于静态代理实现,原理图:

 总的来说后者比前者性能高但复杂,两者对比总结: 

 

3.1. Spring AOP示例

定义切点:@Pointcut、@Before、@After、@AfterReturning、@AfterThrowing、@Around等。

定义切面:@Aspect

两个概念:目标对象(被代理的对象)、代理对象(目标对象的代理对象)

 

3.1.1. 示例

UserRepository及UserRepositoryImpl:

复制代码
package com.marchon.learning.aspect;

public interface UserRepository {
    public Integer addUser(Integer val);

    void updateUser();

    void deleteUser();

    void findUser();
}
UserRepository.java
复制代码
复制代码
package com.marchon.learning.aspect;

import org.springframework.stereotype.Repository;

@Repository
public class UserRepositoryImpl implements UserRepository {

    @Override
    public Integer addUser(Integer val) {
        System.out.println("*add user...*");
        return 10 / val;
    }

    @Override
    public void updateUser() {
        System.out.println("*update user...*");

    }

    @Override
    public void deleteUser() {
        System.out.println("*delet4e user...*");

    }

    @Override
    public void findUser() {
        System.out.println("*update user...*");

    }

}
UserRepositoryImpl.java
复制代码

切面定义与配置:

复制代码
package com.marchon.learning.aspect;

import java.util.Arrays;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

/**
 * 在@Before等注解的value属性上指定pointcut,其他类似;各方法的参数模式应与下面一致(有的无参有的1参)、返回值则可以任意。
 */
@Aspect // 表示一个切面
@Component // 以让Spring IOC容器识别并生成动态代理
public class MyAspect {
    private static final String CLASS_QUALIFIED_NAME = "com.marchon.learning.aspect.UserRepository";

    @Before("execution(* " + CLASS_QUALIFIED_NAME + ".addUser(..))")
    public void before() {
        System.out.println("前置通知...");
    }

    @After("execution(* " + CLASS_QUALIFIED_NAME + ".addUser(..))")
    public void after() {
        System.out.println("后置通知...");
    }

    @AfterReturning(value = "execution(* " + CLASS_QUALIFIED_NAME + ".addUser(..))", returning = "retVal")
    public void afterReturning(Object retVal) {
        System.out.println("后置返回通知..." + retVal);
    }

    @AfterThrowing(value = "execution(* " + CLASS_QUALIFIED_NAME + ".addUser(..))", throwing = "ex")
    public void afterThrowing(Throwable ex) {// 若环绕通知中对目标函数产生的异常进行捕获处理则这里就不再可收到目标函数所抛异常
        System.out.println("后置异常通知..." + ex.getMessage());
    }

    @Around("execution(* " + CLASS_QUALIFIED_NAME + ".addUser(..))")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("环绕通知前... ,参数:" + Arrays.asList(joinPoint.getArgs()));
        Object ret = null;
        ret = joinPoint.proceed();
        System.out.println("环绕通知后...");

//        try {//若这里进行异常捕获处理则后置通知将不再能收到目标方法执行产生的异常
//            ret = joinPoint.proceed();
//            System.out.println("环绕通知后...");
//        } catch (Throwable e) {
//            e.printStackTrace();
//        }
        return ret;
    }

    // ============= 以下为与上面@Before等价的定义
    @Pointcut("execution(* " + CLASS_QUALIFIED_NAME + ".addUser(..))")
    public void pointcut1() {

    }

    @Before("pointcut1()")
    public void b1() {
        System.out.println("等价的 前置通知...");
    }

    // ============= 以下为@Before获取参数的示例,除了@Around外的其他通知获取参数的方式与下类似
//    @Before(value = "execution(* " + CLASS_QUALIFIED_NAME + ".addUser(..)) && args(theVal)", argNames = "theVal") // 两种写法均可
    @Before(value = "execution(* " + CLASS_QUALIFIED_NAME + ".addUser(..)) && args(theVal)") // 下面方法参数名须与此一致
    public void before(Integer theVal) {
        System.out.println("前置通知... ,参数:" + theVal);
    }

}
MyAspect.java
复制代码

执行及结果:

复制代码
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
public class StudentTest {
    @Autowired
    private UserRepository userRepository;

    @Test
    public void student() {
        userRepository.addUser(2);
        System.out.println();
        
        userRepository.addUser(0);
    }

}


//输出如下
环绕通知前... ,参数:[2]
等价的 前置通知...
前置通知... ,参数:2
前置通知...
*add user...*
环绕通知后...
后置通知...
后置返回通知...5

环绕通知前... ,参数:[0]
等价的 前置通知...
前置通知... ,参数:0
前置通知...
*add user...*
后置通知...
后置异常通知.../ by zero
View Code
复制代码

 可以看出无论是否出异常@After通知都会被执行

注:

  • 定义切点的几个注解只能用在方法上。
  • @Pointcut + @Before组合来定义切入点及通知,也可以直接只用@Before等来定义切入点及通知。前者使得在对同一切入点绑定多个通知时不需重复写长长的切入点、后者则可以减少一层代码,采用哪种视情况而定。
  • 获取目标对象的方法参数:@Around可以直接通过ProceedingJoinPoint获取、其他Advice可以通过args参数获取。示例:
        // ============= 以下为@Before获取参数的示例,除了@Around外的其他通知获取参数的方式与下类似
    //    @Before(value = "execution(* " + CLASS_QUALIFIED_NAME + ".addUser(..)) && args(theVal)", argNames = "theVal") // 两种写法均可
        @Before(value = "execution(* " + CLASS_QUALIFIED_NAME + ".addUser(..)) && args(theVal)") // 下面方法参数名须与此一致
        public void before(Integer theVal) {
            System.out.println("前置通知... ,参数:" + theVal);
        }
  • 在@Before等Advice或在@Pointcut中定义切入点。切入点表达式见下节。

 

3.1.2. 切入点表达式(切入点指示符)

(1) Wildcard(通配符): *  、  ..  、 +   

 *  :匹配任意数量的字符。示例:

execution(* set*(int))  //匹配以set开头,参数为int类型,任意返回值的方法 

..  :匹配方法中任意数量的参数、匹配指定包及任意子包。示例:

execution(public * *(..))  //匹配任意返回值,任意名称,任意参数的公共方法
within(com.marchon.learning.aspect..*) //匹配com.marchon.learning.aspect包及其子包中所有类中的所有方法

+  :匹配给定类的任意子类。示例:

within(com.marchon.learning.aspect.UserRepository+) //匹配实现了UserRepository接口的所有子类的方法

(2) Class Signature Expression(类签名表达式):匹配符合指定类型(包名、类名、接口)模式的方法。

语法:  within(<type name>)  ,示例:

@Pointcut("within(com.marchon.learning.aspect..*)") //匹配com.marchon.learning.aspect包及其子包中所有类中的所有方法
@Pointcut("within(com.marchon.learning.aspect.UserRepositoryImpl)") //匹配UserRepositoryImpl类中所有方法
@Pointcut("within(com.marchon.learning.aspect.UserRepositoryImpl+)") //匹配UserRepositoryImpl类及其子类中所有方法
@Pointcut("within(com.marchon.learning.aspect.UserRepository+)") //匹配所有实现UserRepository接口的类的所有方法

(3) Method Signature Expression(方法签名表达式):匹配符合指定方法签名模式的方法。

语法:

//scope :方法作用域,如public,private,protect
//returnt-type:方法返回值类型
//fully-qualified-class-name:方法所在类的完全限定名称
//parameters:方法参数
execution(<scope> <return-type> <fully-qualified-class-name>.*(parameters))

示例:

@Pointcut("execution(* com.marchon.learning.aspect.UserRepositoryImpl.*(..))") //匹配UserRepositoryImpl类中的所有方法
@Pointcut("execution(public * com.marchon.learning.aspect.UserRepositoryImpl.*(..))") //匹配UserRepositoryImpl类中的所有公共的方法
@Pointcut("execution(public int com.marchon.learning.aspect.UserRepositoryImpl.*(..))") //匹配UserRepositoryImpl类中的所有公共方法并且返回值为int类型
@Pointcut("execution(public * com.marchon.learning.aspect.UserRepositoryImpl.*(int , ..))") //匹配UserRepositoryImpl类中第一个参数为int类型的所有公共的方法

(4) bean:匹配指定名称的bean对象的方法(AspectJ中没有此指示符)。示例: @Pointcut("bean(*Service)") //匹配名称以Service结尾的bean中的所有方法 

(5) target:匹配指定类型的被代理对象旳方法。示例: @Pointcut("target(com.marchon.learning.aspect.UserRepository)") //匹配实现了指定接口的所有被代理对象的方法 

(6) this:匹配指定类型的代理对象的旳方法。示例: @Pointcut("this(com.marchon.learning.aspect.UserRepository)") //匹配实现了指定接口的所有代理对象的方法 

(7) @within:匹配被指定注解修饰的类型中的方法。示例: @Pointcut("@within(com.marchon.spring.annotation.MarkerAnnotation)") //匹配被指定注解修饰的类中的方法 

(8) @annotation:匹配被指定注解修饰的方法。示例: @Pointcut("@annotation(com.marchon.spring.annotation.MarkerAnnotation)") //匹配使用了指定注解的方法  

(9) 逻辑运算符:and、or、not (或 &&、||、! )。示例: @Pointcut("bean(*Service) && within(com.marchon.learning) ") //匹配名以Service结尾且在com.marchon.learning包下的bean中的方法 

3.1.3. Advice中获取目标对象方法参数

在Spring AOP中,除了execution和bean指示符不能传递参数给通知方法,其他指示符都可以将匹配的方法相应参数或对象自动传递给通知方法。

可通过args(xx)及argNames="xx"来获取目标对象的示例见3.1.1节获取目标对象参数部分。

对于@Around还可直接通过ProceedingJoinPoint获取目标对象的方法参数等信息。

3.1.4. 通知执行的优先级

同一个切面内的多个通知(可能是不同通知也可能是同一种)作用在同一个切点,或者不同切面中的多个通知作用在同一个切入点时,通知执行具有一定的先后顺序。

(1) 多个切面应用到同一个切入点(目标函数)上时,与切面优先级有关:对于目标函数之前的Advice优先级高的切面先执行、对于目标函数之后的Advice优先级高的后执行。

执行示意图(下图中切面1优先级比切面2高):

示例:定义了AspectOne、AspectTwo两个切面且前者优先级比后者高(优先级通过实现springframework Ordered接口指定),其相应执行结果如下:

复制代码
@Aspect
@Component
class AspectOne implements Ordered {
    private static final String CLASS_QUALIFIED_NAME = "com.marchon.learning.aspect.UserRepository";

    @Pointcut("execution(* " + CLASS_QUALIFIED_NAME + ".updateUser(..))")
    public void myPointcut() {
    }

    @Before("myPointcut()")
    public void before1() {
        System.out.println("One before1");
    }

    @Before("myPointcut()")
    public void before2() {
        System.out.println("One before2");
    }

    @After("myPointcut()")
    public void after1() {
        System.out.println("One after1");
    }

    @After("myPointcut()")
    public void after2() {
        System.out.println("One after2");
    }

    @Override
    public int getOrder() {
        return 0;
    }
}

@Aspect
@Component
class AspectTwo implements Ordered {
    private static final String CLASS_QUALIFIED_NAME = "com.marchon.learning.aspect.UserRepository";

    @Pointcut("execution(* " + CLASS_QUALIFIED_NAME + ".updateUser(..))")
    public void myPointcut() {
    }

    @Before("myPointcut()")
    public void before1() {
        System.out.println("Two before1");
    }

    @Before("myPointcut()")
    public void before2() {
        System.out.println("Two before2");
    }

    @After("myPointcut()")
    public void after1() {
        System.out.println("Two after1");
    }

    @After("myPointcut()")
    public void after2() {
        System.out.println("Two after2");
    }

    @Override
    public int getOrder() {
        return 1;
    }
}

//执行userRepository.updateUser结果如下,说明了作用于同一个切点的不同切面的执行顺序
One before1
One before2
Two before1
Two before2
*update user...*
Two after1
Two after2
One after1
One after2
View Code
复制代码

(2) 同一切面内的多个通知作用在同一切入点(目标函数)时,按 Around、Before、After、AfterReturning、AfterThrowing 的顺序先后执行(故环绕通知始终比前置通知、后置通知等先执行),对于同一种通知,按通知的方法名的字典序顺序执行。示例可见3.1.1中的示例。相关源码见:

复制代码
    private static final Comparator<Method> METHOD_COMPARATOR;

    static {
        Comparator<Method> adviceKindComparator = new ConvertingComparator<>(
                new InstanceComparator<>(
                        Around.class, Before.class, After.class, AfterReturning.class, AfterThrowing.class),
                (Converter<Method, Annotation>) method -> {
                    AspectJAnnotation<?> annotation =
                        AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(method);
                    return (annotation != null ? annotation.getAnnotation() : null);
                });
        Comparator<Method> methodNameComparator = new ConvertingComparator<>(Method::getName);
        METHOD_COMPARATOR = adviceKindComparator.thenComparing(methodNameComparator);
    }
ReflectiveAspectJAdvisorFactory
复制代码

 

 

4. 底层原理

如前面的概念一节中所述,AOP主要包括定义做什么(advice)、定义在哪里做(point)、内部怎样做(ewaving)三部分。

不同的框架实现有所不同。对于Spring AOP而言:先根据pointcut确定哪些目标对象(即被代理对象)要被加入advice,如何加入advise(即如何实现ewaving)?Spring AOP是通过实现后置处理器BeanPostProcessor接口来实现织入的,也就是在bean完成初始化之后,通过动态代理( JDK动态代理或CGLIB动态代理 )为目标对象生成代理对象。

详见前面的原理一节。

 

5. 参考资料

https://blog.csdn.net/javazejian/article/details/56267036

https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#aop

 

posted @   March On  阅读(397)  评论(0编辑  收藏  举报
编辑推荐:
· MySQL 优化利器 SHOW PROFILE 的实现原理
· 在.NET Core中使用异步多线程高效率的处理大量数据
· 聊一聊 C#前台线程 如何阻塞程序退出
· 几种数据库优化技巧
· 聊一聊坑人的 C# MySql.Data SDK
阅读排行:
· 干掉EasyExcel!FastExcel初体验
· 跟着 8.6k Star 的开源数据库,搞 RAG!
· .NET 9 中的 多级缓存 HybridCache
· 夜莺 v8 第一个版本来了,开始做有意思的功能了
· .NET 9 增强 OpenAPI 规范,不再内置swagger
历史上的今天:
2016-07-09 普通用户无法启动监听80端口
2016-07-09 Tomcat 用户访问控制
top last
Welcome user from
(since 2020.6.1)

目录导航

点击右上角即可分享
微信分享提示