AOP面向切面编程(1.6w字长文讲清AOP)

AOP面向切面编程(1.6w字长文讲清AOP),第1张

AOP面向切面编程(1.6w字长文讲清AOP)

AOP面向切面编程
  • 一、代理模式
    • 1. 概念
    • 2. 静态代理
    • 3. 动态代理
  • 二、 AOP的核心套路
  • 三、AOP术语
    • 1. 横切关注点
    • 2. 通知
    • 3. 切面
    • 4. 目标
    • 5. 代理
    • 6. 连接点
    • 7. 切入点
  • 四、基于注解的AOP
    • 1. AOP概念介绍
    • 2. 基于注解的AOP用到的技术
    • 3. 具体 *** 作
      • 3.1 初步实现
      • 3.2 各个通知获取细节信息
      • 3.3 抽取切入点表达式
      • 3.4 切入点表达式语法
      • 3.5 环绕通知
      • 3.6 切面的优先级
      • 3.7 没有接口的情况
      • 小结
  • 五、AOP对获取bean的影响

一、代理模式 1. 概念

① 介绍

二十三种设计模式中的一种,属于结构型模式。它的作用就是通过提供一个代理类,让我们在调用目标方法的时候,不再是直接对目标方法进行调用,而是通过代理类间接调用。让不属于目标方法核心逻辑的代码从目标方法中剥离出来——解耦。调用目标方法时先调用代理对象的方法,减少对目标方法的调用和打扰,同时让附加功能能够集中在一起也有利于统一维护。


使用代理后:


② 日常生活中的“代理”

  • 广告商找大明星拍广告需要经过经纪人
  • 合作伙伴找大老板谈合作要约见面时间需要经过秘书

③ 相关术语

  • 代理:将非核心逻辑剥离出来以后,封装这些非核心逻辑的类、对象、方法。
  • 目标:被代理“套用”了非核心逻辑代码的类、对象、方法。
2. 静态代理

我们有个需求,需要在一个学生类吃饭的方法中实现详细的吃饭过程,首先创建一个Student类,实现EatFood接口的吃东西的方法eat(String foodName),创建静态代理类进行增强:

public interface EatFood {
    void eat();
}

public class Student implements EatFood{
    @Override
    public void eat() {
        System.out.println("开吃啦...");
    }
}
public class EatStaticProxy implements EatFood {
    //将被代理的目标对象声明为成员变量
    private EatFood taget;

    //这里只是方便设置属性
    public EatStaticProxy(EatFood taget) {
        this.taget = taget;
    }

    @Override
    public int eat(String foodName) {
        // 附加功能由代理类中的代理方法来实现
        System.out.println("学生去打饭 *** 作,打的菜是:" + foodName);

        // 通过目标对象来实现核心业务逻辑
        int num = taget.eat(foodName);

        System.out.println("学生吃饱了并把餐具放到指定位置,今日饭菜分数为:" + num);

        return num;
    }

静态代理确实实现了解耦,但是由于代码都写死了,完全不具备任何的灵活性。就拿吃饭功能来说,将来其他人教师、职工也要吃饭,那还得再声明更多个静态代理类,那就产生了大量重复的代码,吃饭功能还是分散的,没有统一管理。

提出进一步的需求:将吃饭功能集中到一个代理类中,将来有任何吃饭需求,都通过这一个代理类来实现。这就需要使用动态代理技术了。

3. 动态代理

为了表达的贴切性,这里用哪个日志功能来演示动态代理,理解动态代理并不是要深入学习它的具体方法,而是要理解他的思想,即:提供一个代理类来代理我们的目标方法进行 *** 作,实现间接调用,结构解耦。


① 生产代理对象的工厂类
JDK本身就支持动态代理,这是反射技术的一部分。下面我们还是创建一个代理类(生产代理对象的工厂类):

// 泛型T要求是目标对象实现的接口类型,本代理类根据这个接口来进行代理
public class LogDynamicProxyFactory {
    
    // 将被代理的目标对象声明为成员变量
    private T target;
    
    public LogDynamicProxyFactory(T target) {
        this.target = target;
    }
    
    public T getProxy() {
    
        // 创建代理对象所需参数一:加载目标对象的类的类加载器
        ClassLoader classLoader = target.getClass().getClassLoader();
    
        // 创建代理对象所需参数二:目标对象的类所实现的所有接口组成的数组
        Class[] interfaces = target.getClass().getInterfaces();
    
        // 创建代理对象所需参数三:InvocationHandler对象
        // Lambda表达式口诀:
        // 1、复制小括号
        // 2、写死右箭头
        // 3、落地大括号
        InvocationHandler handler = (
                                    // 代理对象,当前方法用不上这个对象
                                    Object proxy,
    
                                     // method就是代表目标方法的Method对象
                                     Method method,
    
                                     // 外部调用目标方法时传入的实际参数
                                     Object[] args)->{
    
            // 我们对InvocationHandler接口中invoke()方法的实现就是在调用目标方法
            // 围绕目标方法的调用,就可以添加我们的附加功能
    
            // 声明一个局部变量,用来存储目标方法的返回值
            Object targetMethodReturnValue = null;
    
            // 通过method对象获取方法名
            String methodName = method.getName();
    
            // 为了便于在打印时看到数组中的数据,把参数数组转换为List
            List argumentList = Arrays.asList(args);
    
            try {
    
                // 在目标方法执行前:打印方法开始的日志
                System.out.println("[动态代理][日志] " + methodName + " 方法开始了,参数是:" + argumentList);
    
                // 调用目标方法:需要传入两个参数
                // 参数1:调用目标方法的目标对象
                // 参数2:外部调用目标方法时传入的实际参数
                // 调用后会返回目标方法的返回值
                targetMethodReturnValue = method.invoke(target, args);
    
                // 在目标方法成功后:打印方法成功结束的日志
                System.out.println("[动态代理][日志] " + methodName + " 方法成功结束了,返回值是:" + targetMethodReturnValue);
    
            }catch (Exception e){
    
                // 通过e对象获取异常类型的全类名
                String exceptionName = e.getClass().getName();
    
                // 通过e对象获取异常消息
                String message = e.getMessage();
    
                // 在目标方法失败后:打印方法抛出异常的日志
                System.out.println("[动态代理][日志] " + methodName + " 方法抛异常了,异常信息是:" + exceptionName + "," + message);
    
            }finally {
    
                // 在目标方法最终结束后:打印方法最终结束的日志
                System.out.println("[动态代理][日志] " + methodName + " 方法最终结束了");
    
            }
    
            // 这里必须将目标方法的返回值返回给外界,如果没有返回,外界将无法拿到目标方法的返回值
            return targetMethodReturnValue;
        };
    
        // 创建代理对象
        T proxy = (T) Proxy.newProxyInstance(classLoader, interfaces, handler);
    
        // 返回代理对象
        return proxy;
    }
}
 

这里的思路就是抓住Proxy.newProxyInstance就是生成代理对象的核心方法,需要什么参数我们就提供什么参数。

② 测试

@Test
public void testDynamicProxy() {
    
    // 1.创建被代理的目标对象
    Calculator target = new CalculatorPureImpl();
    
    // 2.创建能够生产代理对象的工厂对象
    LogDynamicProxyFactory factory = new LogDynamicProxyFactory<>(target);
    
    // 3.通过工厂对象生产目标对象的代理对象
    Calculator proxy = factory.getProxy();
    
    // 4.通过代理对象间接调用目标对象
    int addResult = proxy.add(10, 2);
    System.out.println("方法外部 addResult = " + addResult + "n");
    
    int subResult = proxy.sub(10, 2);
    System.out.println("方法外部 subResult = " + subResult + "n");
    
    int mulResult = proxy.mul(10, 2);
    System.out.println("方法外部 mulResult = " + mulResult + "n");
    
    int divResult = proxy.div(10, 2);
    System.out.println("方法外部 divResult = " + divResult + "n");
}
二、 AOP的核心套路

aop的核心套路就是对目标方法进行代理逻辑一层一层的套,将目标方法通过try-catch-finally块包起来,然后通过这个结构我们可以设置方法执行前的 *** 作,方法执行成功后的 *** 作,方法出异常时的 *** 作以及方法最终结束的作用(就是后面学到的前置、返回、异常、后置通知)。甚至于可以将这已经包好的逻辑再当成一层“目标方法”在对其进行系列添加逻辑 *** 作(也就是环绕通知了)

三、AOP术语 1. 横切关注点

从每个方法中抽取出来的同一类非核心业务。在同一个项目中,我们可以使用多个横切关注点对相关方法进行多个不同方面的增强。

这个概念不是语法层面天然存在的,而是根据附加功能的逻辑上的需要:有十个附加功能,就有十个横切关注点。

2. 通知

每一个横切关注点上要做的事情都需要写一个方法来实现,这样的方法就叫通知方法。

  • 前置通知:在被代理的目标方法前执行
  • 返回通知:在被代理的目标方法成功结束后执行
  • 异常通知:在被代理的目标方法异常结束后执行
  • 后置通知:在被代理的目标方法最终结束后执行
  • 环绕通知:使用try…catch…finally结构围绕整个被代理的目标方法,包括上面四种通知对应的所有位置
3. 切面

封装通知方法的类。

4. 目标

被代理的目标类

5. 代理

向目标对象应用通知之后创建的代理对象。

6. 连接点

这也是一个纯逻辑概念,不是语法定义的。

把方法排成一排,每一个横切位置看成x轴方向,把方法从上到下执行的顺序看成y轴,x轴和y轴的交叉点就是连接点。

类里面哪些方法可以被增强,这些方法称为连接点

7. 切入点

定位连接点的方式。

每个类的方法中都包含多个连接点,所以连接点是类中客观存在的事物(从逻辑上来说)。

如果把连接点看作数据库中的记录,那么切入点就是查询记录的 SQL 语句。

Spring 的 AOP 技术可以通过切入点定位到特定的连接点。

切点通过 org.springframework.aop.Pointcut 接口进行描述,它使用类和方法作为连接点的查询条件。

实际被真正增强的方法,称为切入点

四、基于注解的AOP 1. AOP概念介绍

① 名词解释

AOP:Aspect Oriented Programming面向切面编程

② AOP的作用

主要有两点

  • 简化代理:将方法中重复的代理提取出来,让核心方法更专注于自己的功能,提高内聚性
  • 代理增强:把特定的功能封装到切面类中,需要直接套用,被套用了切面逻辑的方法就被切面给增强了。
2. 基于注解的AOP用到的技术

  • 动态代理(InvocationHandler):JDK原生的实现方式,需要被代理的目标类必须实现接口。因为这个技术要求代理对象和目标对象实现同样的接口。
  • cglib:通过继承被代理的目标类实现代理,所以不需要目标类实现接口。
  • AspectJ:本质上是静态代理,将代理逻辑“织入”被代理的目标类编译得到的字节码文件,所以最终效果是动态的。weaver就是织入器。Spring只是借用了AspectJ中的注解。
3. 具体 *** 作 3.1 初步实现

创建maven工程

  1. 加入依赖


        
        
            org.springframework
            spring-context
            5.3.1
        

        
        
            org.springframework
            spring-aspects
            5.3.1
        

        
        
            org.springframework
            spring-test
            5.3.1
        

        
        
            junit
            junit
            4.12
            test
        

    
  1. 准备被代理的目标资源

① 接口

public interface EatFood {
    int eat(String foodName);
}

② 创建目标类

在Spring环境下工作,所有的一切都必须放在IOC容器中。现在接口的实现类是AOP要代理的目标类,所以它也必须放入IOC容器。

@Component
public class Student implements EatFood {
    @Override
    public int eat(String foodName) {
        //模拟学生对饭菜打分
        int score = (int)(Math.random()*9+1);
        System.out.println("学生用晚餐并对本餐进行了打分,今天吃的是:" + foodName + "。分数为:" + score);
        return score;
    }
}
  1. 创建切面类
@Component // @Aspect表示这个类是一个切面类
@Aspect  // @Component注解保证这个切面类能够放入IOC容器
public class EatAspect {

    // @Before注解:声明当前方法是前置通知方法
    // value属性:指定切入点表达式,由切入点表达式控制当前通知方法要作用在哪一个目标方法上
    @Before(value = "execution(public int com.zengchuiyu.aoptest.api.EatFood.eat(String))")
    public void eatBefore(){
        System.out.println("[AOP前置通知]学生在打饭了");
    }

    @AfterReturning(value = "execution(public int com.zengchuiyu.aoptest.api.EatFood.eat(String))")
    public void eatAfterReturning(){
        System.out.println("[AOP返回通知]学生吃完饭将餐具放到指定位置");
    }

    @AfterThrowing(value = "execution(public int com.zengchuiyu.aoptest.api.EatFood.eat(String))")
    public void eatAfterThrowing(){
        System.out.println("[AOP异常通知]学生因故停止了用餐");
    }

    @After(value = "execution(public int com.zengchuiyu.aoptest.api.EatFood.eat(String))")
    public void eatAfter(){
        System.out.println("[AOP最终通知]学生离开了饭堂");
    }

}
  1. 创建Spring的配置文件


    
    
    
    


  1. 测试
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(value = {"classpath:spring-context.xml"})
public class StudentTest {

    @Autowired
    private EatFood eatFood;

    @Test
    public void test1(){
        int score = eatFood.eat("梅菜扣肉");
    }

}
  1. 运行结果


表名我们成功通过注解的方式实现了aop

  1. 通知执行的顺序

Spring版本5.3.x以前:

  • 前置通知
  • 目标 *** 作
  • 后置通知
  • 返回通知或异常通知

Spring版本5.3.x以后:

  • 前置通知
  • 目标 *** 作
  • 返回通知或异常通知
  • 后置通知
3.2 各个通知获取细节信息

回想初步 *** 作,我们可以通过切面类的通知对目标方法进行增强,难受仔细思考难免在增强的时候我们其实是要用到目标方法的详细信息进行 *** 作的,例如方法名、参数列表等,所以不得不引入另外一个接口:JoinPoint接口

  1. JoinPoint接口

org.aspectj.lang.JoinPoint

  • 要点1:JoinPoint接口通过getSignature()方法获取目标方法的签名
  • 要点2:通过目标方法签名对象获取方法名
  • 要点3:通过JoinPoint对象获取外界调用目标方法时传入的实参列表组成的数组

需要获取方法签名、传入的实参等信息时,可以在通知方法声明JoinPoint类型的形参。

方法签名:定义了描述方法信息的一些方法

@Before(value = "execution(public int com.zengchuiyu.aoptest.api.EatFood.eat(String))")
    public void eatBefore(JoinPoint joinPoint){

        // 1.通过JoinPoint对象获取目标方法签名对象
        // 方法的签名:一个方法的全部声明信息
        Signature signature = joinPoint.getSignature();
        // 2.通过方法的签名对象获取目标方法的详细信息
        String methodName = signature.getName();
        System.out.println("methodName = " + methodName);

        int modifiers = signature.getModifiers();
        System.out.println("modifiers = " + modifiers);

        String declaringTypeName = signature.getDeclaringTypeName();
        System.out.println("declaringTypeName = " + declaringTypeName);

        // 3.通过JoinPoint对象获取外界调用目标方法时传入的实参列表
        Object[] args = joinPoint.getArgs();

        // 4.由于数组直接打印看不到具体数据,所以转换为List集合
        List argList = Arrays.asList(args);
        
        System.out.println("[AOP前置通知]学生在打饭了");
    }
 
  1. 方法返回值

在返回通知中获取目标方法返回值分两步:
第一步:在@AfterReturning注解中通过returning属性设置一个名称
第二步:使用returning属性设置的名称在通知方法中声明一个对应的形参

@AfterReturning(
            value = "execution(public int com.zengchuiyu.aoptest.api.EatFood.eat(String))",
            returning = "targetreturnValue"
    )
    public void eatAfterReturning(Object targetreturnValue){
        System.out.println("[AOP返回通知]学生吃完饭将餐具放到指定位置");
    }
  1. 目标方法抛出的异常


在异常通知中获取目标方法抛出的异常分两步:
第一步:在@AfterThrowing注解中声明一个throwing属性设定形参名称
第二步:使用throwing属性指定的名称在通知方法声明形参,Spring会将目标方法抛出的异常对象从这里传给我们

3.3 抽取切入点表达式

① 声明

在一处声明切入点表达式之后,其他有需要的地方引用这个切入点表达式。易于维护,一处修改,处处生效。声明方式如下:

@Pointcut("execution(* *..*.eat(..))")
    public void declarPointCut(){
        
    }

② 同一个类内部引用

@Before(value = "declarPointCut()")
    public void eatBefore(JoinPoint joinPoint){

③ 在不同类中引用

 @Before(value = "com.zengchuiyu.aoptest.aspect.EatCommonAspect.declarPointCut()")
    public void eatBefore(JoinPoint joinPoint){

④ 集中管理

而作为存放切入点表达式的类,可以把整个项目中所有切入点表达式全部集中过来,便于统一管理

3.4 切入点表达式语法
  1. 切入点表达式的作用


2. 语法细节

  • 用*号代替“权限修饰符”和“返回值”部分表示“权限修饰符”和“返回值”不限

  • 在包名的部分,一个“*”号只能代表包的层次结构中的一层,表示这一层是任意的。

例如:*.Hello匹配com.Hello,不匹配com.atguigu.Hello

  • 在包名的部分,使用“*…”表示包名任意、包的层次深度任意

  • 在类名的部分,类名部分整体用*号代替,表示类名任意

  • 在类名的部分,可以使用*号代替类名的一部分

*Service

上面例子表示匹配所有名称以Service结尾的类或接口

  • 在方法名部分,可以使用*号表示方法名任意
  • 在方法名部分,可以使用*号代替方法名的一部分
*Operation

上面例子表示匹配所有方法名以Operation结尾的方法

  • 在方法参数列表部分,使用(…)表示参数列表任意

  • 在方法参数列表部分,使用(int,…)表示参数列表以一个int类型的参数开头

  • 在方法参数列表部分,基本数据类型和对应的包装类型是不一样的

切入点表达式中使用 int 和实际方法中 Integer 是不匹配的

  • 在方法返回值部分,如果想要明确指定一个返回值类型,那么必须同时写明权限修饰符
execution(public int *..*Service.*(.., int))

上面例子是对的,下面例子是错的:

execution(* int *..*Service.*(.., int))

但是public *表示权限修饰符明确,返回值任意是可以的。

对于execution()表达式整体可以使用三个逻辑运算符号

  • execution() || execution()表示满足两个execution()中的任何一个即可
  • execution() && execution()表示两个execution()表达式必须都满足
  • !execution()表示不满足表达式的其他方法
  1. 总结

3.5 环绕通知

环绕通知对应整个try…catch…finally结构,包括前面四种通知的所有功能。(可以理解为把已经加入前四种的目标方法再当成一个目标方法)

// 使用@Around注解标明环绕通知方法
@Around(value = "com.atguigu.aop.aspect.AtguiguPointCut.transactionPointCut()")
public Object manageTransaction(
    
        // 通过在通知方法形参位置声明ProceedingJoinPoint类型的形参,
        // Spring会将这个类型的对象传给我们
        ProceedingJoinPoint joinPoint) {
    
    // 通过ProceedingJoinPoint对象获取外界调用目标方法时传入的实参数组
    Object[] args = joinPoint.getArgs();
    
    // 通过ProceedingJoinPoint对象获取目标方法的签名对象
    Signature signature = joinPoint.getSignature();
    
    // 通过签名对象获取目标方法的方法名
    String methodName = signature.getName();
    
    // 声明变量用来存储目标方法的返回值
    Object targetMethodReturnValue = null;
    
    try {
    
        // 在目标方法执行前:开启事务(模拟)
        System.out.println("[AOP 环绕通知] 开启事务,方法名:" + methodName + ",参数列表:" + Arrays.asList(args));
    
        // 过ProceedingJoinPoint对象调用目标方法
        // 目标方法的返回值一定要返回给外界调用者
        targetMethodReturnValue = joinPoint.proceed(args);
    
        // 在目标方法成功返回后:提交事务(模拟)
        System.out.println("[AOP 环绕通知] 提交事务,方法名:" + methodName + ",方法返回值:" + targetMethodReturnValue);
    
    }catch (Throwable e){
    
        // 在目标方法抛异常后:回滚事务(模拟)
        System.out.println("[AOP 环绕通知] 回滚事务,方法名:" + methodName + ",异常:" + e.getClass().getName());
    
    }finally {
    
        // 在目标方法最终结束后:释放数据库连接
        System.out.println("[AOP 环绕通知] 释放数据库连接,方法名:" + methodName);
    
    }
    
    return targetMethodReturnValue;
}

环绕通知和前置通知,后置通知有着很大的区别,主要有两个重要的区别:

1)目标方法的调用由环绕通知决定,即你可以决定是否调用目标方法,而前置和后置通知 是不能决定的,他们只是在方法的调用前后执行通知而已,即目标方法肯定是要执行的。

1) 目标方法的调用由环绕通知决定,即你可以决定是否调用目标方法,而前置和后置通知 是不能决定的,他们只是在方法的调用前后执行通知而已,即目标方法肯定是要执行的。

2) 环绕通知可以控制返回对象,即你可以返回一个与目标对象完全不同的返回值,虽然这很危险,但是你却可以办到。而后置方法是无法办到的,因为他是在目标方法返回值后调用

使用面向切面来处理一些问公共的问题,比如,权限管理,事务的委托

3.6 切面的优先级

① 概念
相同目标方法上同时存在多个切面时,切面的优先级控制切面的内外嵌套顺序。

  • 优先级高的切面:外面
  • 优先级低的切面:里面

使用@Order注解可以控制切面的优先级:

  • @Order(较小的数):优先级高
  • @Order(较大的数):优先级低


② 实际意义

实际开发时,如果有多个切面嵌套的情况,要慎重考虑。例如:如果事务切面优先级高,那么在缓存中命中数据的情况下,事务切面的 *** 作都浪费了。


此时应该将缓存切面的优先级提高,在事务 *** 作之前先检查缓存中是否存在目标数据。

3.7 没有接口的情况

在目标类没有实现任何接口的情况下,Spring会自动使用cglib技术实现代理。

小结

五、AOP对获取bean的影响

对实现了接口的类应用切面,我们通过

  • 应用了切面后,真正放在IOC容器中的是代理类的对象
  • 目标类并没有被放到IOC容器中,所以根据目标类的类型从IOC容器中是找不到的

    对没实现接口的类应用切面

欢迎分享,转载请注明来源:内存溢出

原文地址:https://54852.com/zaji/3975393.html

(0)
打赏 微信扫一扫微信扫一扫 支付宝扫一扫支付宝扫一扫
上一篇 2022-10-21
下一篇2022-10-21

发表评论

登录后才能评论

评论列表(0条)