栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

第二章 AOP面向切面编程

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

第二章 AOP面向切面编程

第二章 AOP面向切面编程

[第一节 情景设定]

[第二节 代理模式]

[第三节 AOP的核心套路]

[第四节 AOP术语]

[第五节 基于注解的AOP]

[第六节 基于XML的AOP[了解]]

[第七节 AOP对获取bean的影响]

文章目录
  • 第二章 AOP面向切面编程
  • 第一节 情景设定
    • 1、声明接口
    • 2、给接口声明一个纯净版实现
    • 3、再声明一个带日志功能的实现
    • 4、提出问题
      • ①现有代码缺陷
      • ②解决思路
      • ③困难
  • 第二节 代理模式
    • 1、概念
      • ①介绍
      • ②生活中的代理
      • ③相关术语
    • 2、静态代理
    • 3、动态代理
      • ①生产代理对象的工厂类
      • ②测试
      • ③练习
  • 第三节 核心套路
  • 第四节 AOP术语
    • 1、横切关注点
    • 2、通知[记住]
    • 3、切面[记住]
    • 4、目标
    • 5、代理
    • 6、连接点
    • 7、切入点[记住]
  • 第五节 基于注解的AOP
    • 1、基于注解的AOP用到的技术
    • 2、加入依赖
    • 3、准备被代理的目标资源
      • ①接口
      • ②纯净的实现类
    • 4、创建切面类
    • 5、创建Spring的配置文件
    • 6、测试
  • 第六节 基于XML的AOP[了解]
    • 1、准备工作
      • ①加入依赖
      • ②准备代码
    • 2、配置Spring配置文件
    • 3、测试
  • 第七节 AOP对获取bean的影响
  • 一、根据类型获取bean
    • 1、情景一
    • 2、情景二
    • 3、情景三
    • 4、情景四
    • 5、情景五
  • 二、自动装配
    • 1、情景一
    • 2、情景二
    • 3、情景三
    • 4、情景四
    • 5、情景五
  • 三、总结
    • 1、对实现了接口的类应用切面
    • 2、对没实现接口的类应用切面

第一节 情景设定 1、声明接口
public interface Calculator {
    
    int add(int i, int j);
    
    int sub(int i, int j);
    
    int mul(int i, int j);
    
    int div(int i, int j);
    
}
2、给接口声明一个纯净版实现

没有额外功能

package com.atguigu.proxy.imp;
    
import com.atguigu.proxy.api.Calculator;
    
public class CalculatorPureImpl implements Calculator {
    
    @Override
    public int add(int i, int j) {
    
        int result = i + j;
    
        System.out.println("方法内部 result = " + result);
    
        return result;
    }
    
    @Override
    public int sub(int i, int j) {
    
        int result = i - j;
    
        System.out.println("方法内部 result = " + result);
    
        return result;
    }
    
    @Override
    public int mul(int i, int j) {
    
        int result = i * j;
    
        System.out.println("方法内部 result = " + result);
    
        return result;
    }
    
    @Override
    public int div(int i, int j) {
    
        int result = i / j;
    
        System.out.println("方法内部 result = " + result);
    
        return result;
    }
}
3、再声明一个带日志功能的实现

package com.atguigu.proxy.imp;

import com.atguigu.proxy.api.Calculator;

public class CalculatorLogImpl implements Calculator {
    
    @Override
    public int add(int i, int j) {
    
        System.out.println("[日志] add 方法开始了,参数是:" + i + "," + j);
    
        int result = i + j;
    
        System.out.println("方法内部 result = " + result);
    
        System.out.println("[日志] add 方法结束了,结果是:" + result);
    
        return result;
    }
    
    @Override
    public int sub(int i, int j) {
    
        System.out.println("[日志] sub 方法开始了,参数是:" + i + "," + j);
    
        int result = i - j;
    
        System.out.println("方法内部 result = " + result);
    
        System.out.println("[日志] sub 方法结束了,结果是:" + result);
    
        return result;
    }
    
    @Override
    public int mul(int i, int j) {
    
        System.out.println("[日志] mul 方法开始了,参数是:" + i + "," + j);
    
        int result = i * j;
    
        System.out.println("方法内部 result = " + result);
    
        System.out.println("[日志] mul 方法结束了,结果是:" + result);
    
        return result;
    }
    
    @Override
    public int div(int i, int j) {
    
        System.out.println("[日志] div 方法开始了,参数是:" + i + "," + j);
    
        int result = i / j;
    
        System.out.println("方法内部 result = " + result);
    
        System.out.println("[日志] div 方法结束了,结果是:" + result);
    
        return result;
    }
}
4、提出问题 ①现有代码缺陷

针对带日志功能的实现类,我们发现有如下缺陷:

  • 对核心业务功能有干扰,导致程序员在开发核心业务功能时分散了精力
  • 附加功能分散在各个业务功能方法中,不利于统一维护
②解决思路

解决这两个问题,核心就是:解耦。我们需要把附加功能从业务功能代码中抽取出来。

③困难

解决问题的困难:要抽取的代码在方法内部,靠以前把子类中的重复代码抽取到父类的方式没法解决。所以需要引入新的技术。

第二节 代理模式 1、概念 ①介绍

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

使用代理后:

②生活中的代理
  • 广告商找大明星拍广告需要经过经纪人
  • 合作伙伴找大老板谈合作要约见面时间需要经过秘书
  • 房产中介是买卖双方的代理
③相关术语
  • 代理:将非核心逻辑剥离出来以后,封装这些非核心逻辑的类、对象、方法。
  • 目标:被代理“套用”了非核心逻辑代码的类、对象、方法。

理解代理模式、AOP的核心关键词就一个字:套

2、静态代理

创建静态代理类:

public class CalculatorStaticProxy implements Calculator {
    
    // 将被代理的目标对象声明为成员变量
    private Calculator target;
    
    public CalculatorStaticProxy(Calculator target) {
        this.target = target;
    }
    
    @Override
    public int add(int i, int j) {
    
        // 附加功能由代理类中的代理方法来实现
        System.out.println("[日志] add 方法开始了,参数是:" + i + "," + j);
    
        // 通过目标对象来实现核心业务逻辑
        int addResult = target.add(i, j);
    
        System.out.println("[日志] add 方法结束了,结果是:" + addResult);
    
        return addResult;
    }
    ……

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

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

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;
    }
}
 
②测试 
@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");
}
③练习

动态代理的实现过程不重要,重要的是使用现成的动态代理类去套用到其他目标对象上。

声明另外一个接口:

public interface SoldierService {
    
    int saveSoldier(String soldierName);
    
    int removeSoldier(Integer soldierId);
    
    int updateSoldier(Integer soldierId, String soldierName);
    
    String getSoldierNameById(Integer soldierId);
    
}

给接口一个实现类:

public class SoldierServiceImpl implements SoldierService {
    
    @Override
    public int saveSoldier(String soldierName) {
    
        System.out.println("核心业务逻辑:保存到数据库……");
    
        return 1;
    }
    
    @Override
    public int removeSoldier(Integer soldierId) {
    
        System.out.println("核心业务逻辑:从数据库删除……");
    
        return 1;
    }
    
    @Override
    public int updateSoldier(Integer soldierId, String soldierName) {
    
        System.out.println("核心业务逻辑:更新……");
    
        return 1;
    }

    @Override
    public String getSoldierNameById(Integer soldierId) {
    
        System.out.println("核心业务逻辑:查询数据库……");
    
        return "good";
    }
}

测试:

@Test
public void testSoldierServiceDynamicProxy() {
    
    // 1.创建被代理的目标对象
    SoldierService soldierService = new SoldierServiceImpl();
    
    // 2.创建生产代理对象的工厂对象
    LogDynamicProxyFactory factory = new LogDynamicProxyFactory<>(soldierService);
    
    // 3.生产代理对象
    SoldierService proxy = factory.getProxy();
    
    // 4.通过代理对象调用目标方法
    String soldierName = proxy.getSoldierNameById(1);
    System.out.println("soldierName = " + soldierName);
    
}
第三节 核心套路

第四节 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用到的技术

  • 动态代理(InvocationHandler):JDK原生的实现方式,需要被代理的目标类必须实现接口。因为这个技术要求代理对象和目标对象实现同样的接口(兄弟两个拜把子模式)。
  • cglib:通过继承被代理的目标类(认干爹模式)实现代理,所以不需要目标类实现接口。
  • AspectJ:本质上是静态代理,将代理逻辑“织入”被代理的目标类编译得到的字节码文件,所以最终效果是动态的。weaver就是织入器。Spring只是借用了AspectJ中的注解。
2、加入依赖
    
        
        
            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
        
    
3、准备被代理的目标资源 ①接口
public interface Calculator {
    
    int add(int i, int j);
    
    int sub(int i, int j);
    
    int mul(int i, int j);
    
    int div(int i, int j);
    
}
②纯净的实现类

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

package com.atguigu.aop.imp;
    
import com.atguigu.aop.api.Calculator;
import org.springframework.stereotype.Component;
    
@Component
public class CalculatorPureImpl implements Calculator {
    
    @Override
    public int add(int i, int j) {
    
        int result = i + j;
    
        System.out.println("方法内部 result = " + result);
    
        return result;
    }
    
    @Override
    public int sub(int i, int j) {
    
        int result = i - j;
    
        System.out.println("方法内部 result = " + result);
    
        return result;
    }
    
    @Override
    public int mul(int i, int j) {
    
        int result = i * j;
    
        System.out.println("方法内部 result = " + result);
    
        return result;
    }
    
    @Override
    public int div(int i, int j) {
    
        int result = i / j;
    
        System.out.println("方法内部 result = " + result);
    
        return result;
    }
}
4、创建切面类
// @Aspect表示这个类是一个切面类
@Aspect
// @Component注解保证这个切面类能够放入IOC容器
@Component
public class LogAspect {
        
    // @Before注解:声明当前方法是前置通知方法
    // value属性:指定切入点表达式,由切入点表达式控制当前通知方法要作用在哪一个目标方法上
    @Before(value = "execution(public int com.atguigu.aop.api.Calculator.add(int,int))")
    public void printLogBeforeCore() {
        System.out.println("[AOP前置通知] 方法开始了");
    }
    
    @AfterReturning(value = "execution(public int com.atguigu.aop.api.Calculator.add(int,int))")
    public void printLogAfterSuccess() {
        System.out.println("[AOP返回通知] 方法成功返回了");
    }
    
    @AfterThrowing(value = "execution(public int com.atguigu.aop.api.Calculator.add(int,int))")
    public void printLogAfterException() {
        System.out.println("[AOP异常通知] 方法抛异常了");
    }
    
    @After(value = "execution(public int com.atguigu.aop.api.Calculator.add(int,int))")
    public void printLogFinallyEnd() {
        System.out.println("[AOP后置通知] 方法最终了");
    }
    
}
5、创建Spring的配置文件


    
    
    
    
    
    
    

6、测试
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(value = {"classpath:applicationContext.xml"})
public class AOPTest {
    
    @Autowired
    private Calculator calculator;
    
    @Test
    public void testAnnotationAOP() {
    
        int add = calculator.add(10, 2);
        System.out.println("方法外部 add = " + add);
    
    }
    
}
第六节 基于XML的AOP[了解] 1、准备工作 ①加入依赖

和基于注解的AOP时一样。

②准备代码

把测试基于注解功能时的Java类复制到新module中,去除所有注解。

2、配置Spring配置文件


    


    


    
    
    
    
    
    
    
        
        
        
        
    
        
        
        
    
        
        
        
    
        
        
    
        
        
    
    

3、测试
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(value = {"classpath:spring-context.xml"})
public class AOPTest {
    
    @Autowired
    private Calculator calculator;
    
    @Test
    public void testLogAspect() {
        int add = calculator.add(10, 2);
        System.out.println("add = " + add);
    }
}
第七节 AOP对获取bean的影响 一、根据类型获取bean 1、情景一
  • bean对应的类没有实现任何接口

  • 根据bean本身的类型获取bean

    • 测试:IOC容器中同类型的bean只有一个

      正常获取到IOC容器中的那个bean对象

    • 测试:IOC容器中同类型的bean有多个

      会抛出NoUniqueBeanDefinitionException异常,表示IOC容器中这个类型的bean有多个

2、情景二
  • bean对应的类实现了接口,这个接口也只有这一个实现类
    • 测试:根据接口类型获取bean
    • 测试:根据类获取bean
    • 结论:上面两种情况其实都能够正常获取到bean,而且是同一个对象
3、情景三
  • 声明一个接口

  • 接口有多个实现类

  • 接口所有实现类都放入IOC容器

    • 测试:根据接口类型获取bean

      会抛出NoUniqueBeanDefinitionException异常,表示IOC容器中这个类型的bean有多个

    • 测试:根据类获取bean

      正常

4、情景四
  • 声明一个接口
  • 接口有一个实现类
  • 创建一个切面类,对上面接口的实现类应用通知
    • 测试:根据接口类型获取bean
    • 测试:根据类获取bean

原因分析:

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

从内存分析的角度来说,IOC容器中引用的是代理对象,代理对象引用的是目标对象。IOC容器并没有直接引用目标对象,所以根据目标类本身在IOC容器范围内查找不到。

**
**

debug查看代理类的类型:

5、情景五
  • 声明一个类
  • 创建一个切面类,对上面的类应用通知
    • 测试:根据类获取bean,能获取到

debug查看实际类型:

二、自动装配

自动装配需先从IOC容器中获取到唯一的一个bean才能够执行装配。所以装配能否成功和装配底层的原理,和前面测试的获取bean的机制是一致的。

1、情景一
  • 目标bean对应的类没有实现任何接口

  • 根据bean本身的类型装配这个bean

    • 测试:IOC容器中同类型的bean只有一个

      正常装配

    • 测试:IOC容器中同类型的bean有多个

      会抛出NoUniqueBeanDefinitionException异常,表示IOC容器中这个类型的bean有多个

2、情景二
  • 目标bean对应的类实现了接口,这个接口也只有这一个实现类

    • 测试:根据接口类型装配bean

      正常

    • 测试:根据类装配bean

      正常

3、情景三
  • 声明一个接口

  • 接口有多个实现类

  • 接口所有实现类都放入IOC容器

    • 测试:根据接口类型装配bean

      @Autowired注解会先根据类型查找,此时会找到多个符合的bean,然后根据成员变量名作为bean的id进一步筛选,如果没有id匹配的,则会抛出NoUniqueBeanDefinitionException异常,表示IOC容器中这个类型的bean有多个

    • 测试:根据类装配bean

      正常

4、情景四
  • 声明一个接口

  • 接口有一个实现类

  • 创建一个切面类,对上面接口的实现类应用通知

    • 测试:根据接口类型装配bean

      正常

    • 测试:根据类装配bean

      此时获取不到对应的bean,所以无法装配,抛出下面的异常:

Caused by: org.springframework.beans.factory.BeanNotOfRequiredTypeException: Bean named ‘fruitApple’ is expected to be of type ‘com.atguigu.bean.impl.FruitAppleImpl’ but was actually of type ‘com.sun.proxy.$Proxy15’

5、情景五
  • 声明一个类

  • 创建一个切面类,对上面的类应用通知

    • 测试:根据类装配bean

      正常

三、总结 1、对实现了接口的类应用切面

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

转载请注明:文章转载自 www.mshxw.com
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号