15.Spring 面向切面编程

Spring 面向切面编程

AOP 是 Spring 框架除了 IOC 之外的另一个核心概念。

  AOP:Aspect Oriented Programming,意为面向切面编程。这是一个新的概念,但是我们知道 Java 是面向对象编程(OOP:Object Oriented Programming)的,指将所有的一切都看作对象,通过对象与对象之间相互作用来解决问题。AOP 是对 OOP 的一个补充,是在另外一个维度抽象出来的对象,具体是指程序在运行时,动态地将非业务代码切入业务代码中,从而实现代码的解耦合,将非业务代码抽象成一个对象,对该对象进行编程,这就是面向切面编程。

在这里插入图片描述

AOP 的优点:

  1. 降低模块之间的耦合度;
  2. 提高了代码的可维护性;
  3. 提高了代码的复用性;
  4. 集中分开管理非业务代码和业务代码,逻辑更加清晰;
  5. 业务代码更加简洁纯粹,没有其他代码的影响;

1. 面向切面编程思想

  介绍概念过于抽象和空泛,不易于理解,下面我们通过一个实例来说明,慢慢引出 AOP 的动态代理机制以及实际开发中如何使用面向切面编程。

1、创建一个计算器接口 Cal,定义四个方法:加、减、乘、除;

public interface Cal {
    public int add(int num1,int num2);
    public int sub(int num1,int num2);
    public int mul(int num1,int num2);
    public int div(int num1,int num2);
}

2、创建接口实现类 CalImpl,并且实现上面定义的四个方法;

public class CalImpl implements Cal{

    public int add(int num1, int num2) {
        int result = num1 + num2;
        return result;
    }

    public int sub(int num1, int num2) {
        int result = num1 - num2;
        return result;
    }

    public int mul(int num1, int num2) {
        int result = num1 * num2;
        return result;
    }

    public int div(int num1, int num2) {
        int result = num1 / num2;
        return result;
    }
}

3、在测试方法中创建 CalImpl 对象,调用方法;

public class Test3 {
    public static void main(String[] args) {
        Cal cal = new CalImpl();
        cal.add(12,4); //16
        cal.sub(12,4); //8
        cal.mul(12,4); //48
        cal.div(12,4); //3
    }
}

以上这段代码很简单,现在想要添加功能:在每一个方法执行的同时,打印日志信息,即该方法的参数列表和该方法的计算结果

这个需求很简单,只需要在每一个方法体中,运算执行之前打印参数列表,运行之后打印计算结果即可,因此,对代码做出如下修改:

public class CalImpl implements Cal{

    public int add(int num1, int num2) {
        System.out.println("add方法的参数是["+num1+","+num2+"]" );
        int result = num1 + num2;
        System.out.println("add方法的结果是"+result);
        return result;
    }

    public int sub(int num1, int num2) {
        System.out.println("sub方法的参数是["+num1+","+num2+"]" );
        int result = num1 - num2;
        System.out.println("sub方法的结果是"+result);
        return result;
    }

    public int mul(int num1, int num2) {
        System.out.println("mul方法的参数是["+num1+","+num2+"]" );
        int result = num1 * num2;
        System.out.println("mul方法的结果是"+result);
        return result;
    }

    public int div(int num1, int num2) {
        System.out.println("div方法的参数是["+num1+","+num2+"]" );
        int result = num1 / num2;
        System.out.println("div方法的结果是"+result);
        return result;
    }
}

再次运行代码,成功打印日志信息:

在这里插入图片描述

  功能已经实现了,但是我们会发现这种方式业务代码和打印日志代码的耦合性非常高,不利于代码后期的维护。例如,如果需求发生改变,需要对打印的日志内容作出修改,那么我们就必须修改4个方法中的所有相关代码,如果是100个方法呢?每次就需要手动去修改100个方法中的代码,对项目的维护成本就相当高。

  从这个例子中,我们会发现4个打印日志信息的代码基本相同,那么有没有可能将这部分的代码抽取出来进行封装,统一进行维护呢?同时也可以将日志代码和业务代码完全分离,解耦合。

  按照这个思路继续抽象,我们希望做的事情就是把这4个方法的相同位置(业务方法执行前、业务方法执行后)提取出来,形成一个横切面,并且将这个横切面封装成一个对象,将所有的打印日志代码写到这个对象中,以实现与业务代码的分离,这就是面向切面编程的思想。

2. 动态代理实现AOP

  根据上述的需求,我们希望在 CalImpl 中只进行业务运算,不进行打印日志的工作,那么就需要有一个对象来代替 CalImpl 进行打印日志的工作,这就是代理对象。所以一个很直观的实现思路就是使用动态代理的方式。

1、删除 CalImpl 方法中所有的打印日志代码,只保留业务代码;

public class CalImpl implements Cal{

    public int add(int num1, int num2) {
        int result = num1 + num2;
        return result;
    }

    public int sub(int num1, int num2) {
        int result = num1 - num2;
        return result;
    }

    public int mul(int num1, int num2) {
        int result = num1 * num2;
        return result;
    }

    public int div(int num1, int num2) {
        int result = num1 / num2;
        return result;
    }
}

2、创建 MyInvocationHandler 类,并实现 InvocationHandler 接口,成为一个动态代理类;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;

public class MyInvocationHandler implements InvocationHandler {
    //委托对象,因为不知道委托对象的类型,所以定义成Object(多态)
    private Object object = null;

    //返回代理对象
    public Object bind(Object object){
        this.object = object;
        return Proxy.newProxyInstance(object.getClass().getClassLoader(),
                object.getClass().getInterfaces(),
                this);
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println(method.getName()+"的参数是:"+ Arrays.toString(args));
        Object result = method.invoke(object,args);
        System.out.println(method.getName()+"的结果是:"+result);
        return result;
    }
}

bind 方法是 MyInvocationHandler 类提供给外部调用的方法,传入需要被代理的对象,bind 方法会返回一个代理对象。bind 方法完成了两项工作:

  1. 将外部传进来的委托对象保存到成员变量中,因为业务方法调用时需要用到委托对象。

  2. 通过 Proxy.newProxyInstance 方法创建一个代理对象:

    解释一下 Proxy.newProxyInstance 方法的参数:

    • 我们知道对象是 JVM 根据运行时类来创建的,此时需要动态创建一个代理对象,可以使用委托对象的运行时类来创建代理对象object.getClass().getClassLoader() 获取委托对象的运行时类;
    • 同时代理对象需要具备委托对象的所有方法,即需要用于委托对象的所有接口,所以传入 object.getClass().getInterfaces()
    • this 指的是当前 MyInvocationHandler 类的对象;

invoke 方法:method 是描述委托对象的所有方法的对象,args 是描述委托对象方法的参数列表的对象。

method.invoke(object,args) 是通过反射机制来调用被代理对象的方法,即业务方法

所以在 method.invoke(object, args) 前后添加打印日志的信息,就等同于在委托对象的业务方法前后添加打印日志信息,并且已经做到了分离,业务方法在委托对象中,打印日志信息在代理对象中。

3、测试方法中执行代码

public class Test3 {
    public static void main(String[] args) {
        //委托对象
        Cal cal = new CalImpl();
        MyInvocationHandler handler = new MyInvocationHandler();
        //代理对象
        Cal cals = (Cal)handler.bind(cal);
        cals.add(12,4);
        cals.sub(12,4);
        cals.mul(12,4);
        cals.div(12,4);
    }
}

在这里插入图片描述

可以看到和上面的执行结果一样,但是我们现在已经做到了代码分离,CalImpl 类中只有业务方法,打印日志的代码写在了 MyInvocationHandler 类中。

以上就是通过动态代理实现 AOP 的过程,我们在使用 Spring 框架的 AOP 时,并不需要那么复杂,Spring 已经对这个过程进行了封装,让开发者可以更加便捷的使用 AOP 进行开发。


3. Spring 框架的AOP操作

  在 Spring 框架中,我们不需要创建动态代理类,只需要创建一个切面类,Spring 底层自动会根据切面类以及目标类生成一个代理对象。

第一步:在 pom.xml 配置文件中添加 aspect 注解相关的依赖

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.5</version>
</dependency>

第二步:创建切面类 LoggerAspect

@Component
@Aspect
public class LoggerAspect {

    @Before("execution(public int com.trainingl.aop.CalImpl.*(..))")
    public void before(JoinPoint joinPoint){
        //获取方法名
        String name = joinPoint.getSignature().getName();
        //获取参数列表
        String args = Arrays.toString(joinPoint.getArgs());
        System.out.println(name + "的参数是:" + args);
    }

    @After("execution(public int com.trainingl.aop.CalImpl.*(..))")
    public void after(JoinPoint joinPoint){
        //获取方法名
        String name = joinPoint.getSignature().getName();
        System.out.println(name+"方法结束");
    }

    @AfterReturning(value = "execution(public int com.trainingl.aop.CalImpl.*(..))",returning = "result")
    public void afterReturn(JoinPoint joinPoint, Object result){
        //获取方法名
        String name = joinPoint.getSignature().getName();
        System.out.println(name+"方法的结果是"+result);
    }

    @AfterThrowing(value = "execution(public int com.trainingl.aop.CalImpl.*(..))",throwing = "exception")
    public void afterThrowing(JoinPoint joinPoint,Exception exception){
        //获取方法名
        String name = joinPoint.getSignature().getName();
    }
}

LoggerAspect 类名处添加了两个注解:

  1. @Aspect:表示该类是切面类;
  2. @Component:将该类注入到 IOC 容器;

分别来说明类中的 4 个方法的注解的含义:

@Before("execution(public int com.trainingl.aop.CalImpl.*(..))")
public void before(JoinPoint joinPoint){
    //获取方法名
    String name = joinPoint.getSignature().getName();
    //获取参数列表
    String args = Arrays.toString(joinPoint.getArgs());
    System.out.println(name + "的参数是:" + args);
}
  1. @Before:表示 before 方法执行的时机;
  2. execution(public int com.trainingl.aop.CalImpl.*(..)):表示切入点是 com.trainingl.aop 包下 CalImpl 类中的所有方法;

@Before 表示 CalImpl 所有方法在执行之前会首先执行 LoggerAspect 类中的 before 方法。

@after 注解同理,表示 CalImpl 所有方法在执行之后会首先执行 LoggerAspect 类中的 after 方法;

@afterReturning 注解表示 CalImpl 所有方法在 return 之后会执行 LoggerAspect 类中的方法;

@afterThrowing 注解表示 CalImpl 所有方法在抛出异常时会执行 LoggerAspect 类中的 afterThrowing 方法。

所以,开发者可以根据具体的切入需求,选择在 before、after、afterReturn、afterThrowing 方法中添加相应的代码。

第三步:目标类也需要添加 @Component 注解

@Component
public class CalImpl implements Cal{

    public int add(int num1, int num2) {
        int result = num1 + num2;
        return result;
    }

    public int sub(int num1, int num2) {
        int result = num1 - num2;
        return result;
    }

    public int mul(int num1, int num2) {
        int result = num1 * num2;
        return result;
    }

    public int div(int num1, int num2) {
        int result = num1 / num2;
        return result;
    }
}

第四步:spring.xml 中进行配置

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:p="http://www.springframework.org/schema/p" xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
	   http://www.springframework.org/schema/context
	   http://www.springframework.org/schema/context/spring-context-4.3.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
    <!--将指定包下的类扫描到IOC容器中-->
    <context:component-scan base-package="com.trainingl.aop"></context:component-scan>

    <!--使Aspect注解生效,为目标类自动生成代理对象-->
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>
  1. com.trainingl.ioc 包中的类扫描到 IOC 容器中;

  2. 添加 aop:aspectj-autoproxy 注解,Spring 容器会结合切面类和目标类自动生成动态代理对象,Spring 框架的 AOP 底层就是通过动态代理的方式完成 AOP;

第五步:测试方法执行如下代码

从 IOC 容器中获取代理对象,执行方法

public class Test4 {
    public static void main(String[] args) {
        //加载spring.xml
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
        //获取代理对象
        Cal cal = (Cal) applicationContext.getBean(Cal.class);
        cal.add(12,4);
        cal.sub(12,4);
        cal.mul(12,4);
        cal.div(12,4);
    }
}

执行结果如下:

在这里插入图片描述

结合代码和图示,重新理解几个概念:

在这里插入图片描述

  • 切面:横切关注点被模块化的特殊对象,即本例中 CalImpl 所有方法中需要加入日志的部分,抽象成一个切面对象 LoggerAspect;
  • 切点:AOP 通过切点定位到连接点;
  • 连接点:程序要执行的某个特定位置,切面方法要插入业务代码的具体位置。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值