Spring AOP 超详细分析

Spring核心机制之 AOP

1. Spring AOP简介

众所周知,Spring的两大核心机制为:

  • IoC
  • AOP (Aspect Oriented Programming,面向切面编程)

本篇博客就来细细品一下AOP的实现以及它的原理;

AOP意味面向切面编程,我们之前一直使用的是OOP(面向对象编程):

  • OOP:将程序中所有参与模块都抽象化为对象,然后通过对象之间的相互调用来完成业务需求;
  • AOP:是对OOP的一个补充,是在另外一个维度上抽象出对象,具体是指程序运行时动态的将非业务代码切入到业务代码中,从而实现代码的解耦和;
    在这里插入图片描述

AOP的优点

  • 降低模块耦合度
  • 使系统容易扩展
  • 延迟设计决定:使用AOP,设计师可以推迟为将来的需求作决定,因为需求作为独立的方面很容易实现
  • 更好的代码复用性

2. AOP例子实践

1. 案例起源

创建一个计算机接口Com,定义以下四个方法:

public interface Com {
    int add(int x, int y);
    int sub(int x, int y);
    int mul(int x, int y);
    int div(int x, int y);
}

定义其实现类:ComImpl:

public class ComImpl implements Com {
    public int add(int x, int y) {
        int result = x+y;
        return result;
    }

    public int sub(int x, int y) {
        int result = x-y;
        return result;
    }

    public int mul(int x, int y) {
        int result = x*y;
        return result;
    }

    public int div(int x, int y) {
        int result = x/y;
        return result;
    }
}

测试类:

public class Test {
    public static void main(String[] args) {
        Com com = new ComImpl();
        System.out.println(com.add(1,4));
    }
}

输出:
5
15

这个例子很简单,这里不做描述了,接下来看新的需求

  • 要求在每个方法执行的同时,完成打印日志信息;

这个需求也很简单,我们可以通过在ComImpl类中的四个方法中每个方法都添加如下:

    public int add(int x, int y) {
        System.out.println("add方法的参数是:"+x+","+y);
        int result = x+y;
        System.out.println("add方法的结果为:"+result);
        return result;
    }

其他三个方法也类似,我就不再写其代码,这种方法能完成业务需求,但是其弊端显著:

  • 重复代码过多,代码冗余;
  • 业务代码和打印日志代码耦合性非常高,不利于后期的维护;
    • 例如: 假如我要对打印日志的格式稍作修改,我就得去改变四个方法中得打印日志部分,假如有100个方法呢?每次都要手动去修改100个方法中的日志打印这块?

那么如何解决这个问题呢?
我们可以发现日志打印的代码基本都是一个格式的,我们能不能将这些相同部分的代码提取出来形成一个横切面呢?并且将这个横切面抽象成一个对象,将所有的打印日志代码写到这个对象中,以实现业务和代码的分离;
上面就是AOP的思想 ☝☝☝☝

2. 静态代理实现AOP

静态代理的要求:

  • 代理对象和被代理对象实现同一个接口,接口中包含着真实业务;
  • 代理对象注入被代理对象,同时可以添加辅助业务;

在这里插入图片描述

缺点:实质上还是比较繁杂,因为你还是需要在代理类的每个真实业务中添加自己的辅助业务,这样还是有许多重复的代码,不便于扩展;

3. 动态代理实现AOP

上面的思想我们可以用动态代理来实现;

对于ComImpl,我们只保留其业务代码(即最初的版本,不在其中添加日志代码);

1. 动态代理类的实现

创建MyInvocationHandler类,这个类实现InvocationHandler接口,成为一个动态代理类:

public class MyInvocationHandler implements InvocationHandler {
    Object targetObj;
    //返回代理对象
    public Object bind(Object targetObj) {
        this.targetObj = targetObj;
        return Proxy.newProxyInstance(this.targetObj.getClass().getClassLoader(), targetObj.getClass().getInterfaces(),
                this);
    }
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Object result = null;
        //日志业务
        System.out.println(method.getName()+"的参数是:"+Arrays.toString(args));
        //主业务
        result = method.invoke(this.targetObj, args);
        //日志业务
        System.out.println(method.getName()+"的结果是:"+result);
        return result;
    }
}

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

  • (1)将外部传进来的委托对象保存到成员变量中,因为业务方法调用时需要用到委托对象。
  • (2)通过 Proxy.newProxyInstance 方法创建一个代理对象,解释一下 Proxy.newProxyInstance 方法的参数:
    • 我们知道对象是 JVM 根据运行时类来创建的,此时需要动态创建一个代理对象的运行时类,同时需要将这个动态创建的运行时类加载到 JVM 中,这一步需要获取到类加载器才能实现,我们可以通过委托对象的运行时类来反向获取类加载器,obj.getClass().getClassLoader() 就是通过委托对象的运行时类来获取类加载器的具体实现;
    • 同时代理对象需要具备委托对象的所有功能,即需要拥有委托对象的所有接口,因此传入obj.getClass().getInterfaces();
    • this 指当前 MyInvocationHandler 对象。

以上全部是反射的知识点,invoke 方法:method 是描述委托对象所有方法的对象,agrs 是描述委托对象方法参数列表的对象。 method.invoke(this.obj,args) 是通过反射机制来调用委托对象的方法,即业务方法。 因此在 method.invoke(this.obj, args) 前后添加打印日志信息,就等同于在委托对象的业务方法前后添加打印日志信息,并且已经做到了分类,业务方法在委托对象中,打印日志信息在代理对象中;

2. 测试

给出测试类:

public class Test {
    public static void main(String[] args) {
        //真实业务类
        Com com = new ComImpl();
        //代理类的对象(这个不叫代理对象)
        MyInvocationHandler myInvocationHandler = new MyInvocationHandler();
        //根据代理类的对象获取代理对象
        Com com1 = (Com) myInvocationHandler.bind(com);
        com1.add(1,5);
        com1.sub(6, 3);
    }
}

输出:
add的参数是:[1, 5]
add的结果是:6
sub的参数是:[6, 3]
sub的结果是:3

3. 结果分析

从测试中我们可以看到已经达到了我们的要求,业务和日志都能正确实现,而且:

  • 业务和日志代码分离,ComImpl类中只有业务代码,而日志代码在MyInvocationHandler类中;

3. Spring中的AOP

在上面的案例中,我们用动态代理实现了AOP,但是在Spring中,我们不需要创建MyInvocationHandler类,Spring已经对其完成了封装,我们只需要创建一个切面类,Spring底层会自动根据切面类以及目标类生成一个代理对象;

1. 第一步:创建一个切面类:LoggerAspect

@Aspect
@Component
public class LoggerAspect {
    //int代表返回值类型,必须写,后面是具体类名,*代表这个类的所有方法,..代表所有参数
    @Before("execution(public int www.springAOP.ComImpl.*(..))")
    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 www.springAOP.ComImpl.*(..))")
    public void after(JoinPoint joinPoint) {
        System.out.println("方法结束");
    }

    @AfterReturning(value = "execution(public int www.springAOP.ComImpl.*(..))", returning = "result")
    //注意上面的result必须与下面的形参名一模一样,且必须有这个形参
    public void afterReturn(JoinPoint joinPoint, Object result) {
        String name = joinPoint.getSignature().getName();
        System.out.println(name+"方法结果为:"+result);
    }

    @AfterThrowing(value = "execution(public int www.springAOP.ComImpl.*(..))", throwing = "ex")
    public void afterThrow(JoinPoint joinPoint, Exception ex) {
        String name = joinPoint.getSignature().getName();
        System.out.println(name+"方法抛出异常:"+ex);
    }
}

下面来分别解释以下各个注解:

  • @Aspect : 声明该类为切面类
    @Component :将该类注入到IoC容器
    (注意:被切入的那个类,即ComImpl必须加上@Component注解⭐)
  • @Before :表示before方法执行的时机
    • execution表达式: public可以省略,int代表返回值,不可以省略,int可以用*来代替,即所有返回值;
      www.springAOP.ComImpl代表需要代理的真实类的全路径,紧跟后面的.代表这个类的所有方法,如果你要指定方法,可以将改成你所指定的方法名;后面的()里面代表参数,…代表所有参数,同样的,你要指定参数,可以将…换成指定的参数类型;
    • execution后面的表达式就是一个范围,代表在这个范围内进行切入;上面的就代表ComImpl所有方法在执行前都会执行LoggerAspect中的before方法;
  • @after :同理,表示 ComImpl 所有方法执行之后会执行 LoggerAspect 类中的 after 方法;
  • @afterReturn : 表示 ComImpl 所有方法在 return 之后会执行 LoggerAspect 类中的 afterReturn 方法;
  • @afterThrowing :表示 ComImpl 所有方法在抛出异常时会执行 LoggerAspect 类中的 afterThrowing 方法;

2. XML中的配置

在applicationContext_AOP.xml中进行如下配置:

	<context:component-scan base-package="www.springAOP"/>

    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>

第一行就不用说了,自动扫描包,使用注解的时候都必须用到这一行;

第二行则是:

  • 使Aspect注解生效,为目标类自动生成代理对象;

    • Spring容器会结合切面类和目标类自动生成代理对象,Spring框架的底层就是通过动态代理的方式完成AOP;

    • 目标类其实就是在那些execution表达式里;

3. 测试

public class AOPTest {
    ApplicationContext applicationContext;
    @Before
    public void testInitial() {
        applicationContext = new ClassPathXmlApplicationContext
                ("applicationContext_AOP.xml");
    }
    @Test
    public void testMethod() {
        //注意这里返回的是代理类,这个代理类继承了Com这个接口的
        //这里返回的不是ComImpl,所以前面的类型只能是Com
        Com com = (Com) applicationContext.getBean("comImpl");
        com.div(2,1);
        System.out.println("---------------------------------");
        com.add(2,5);
        System.out.println("---------------------------------");
        com.div(2,0);
    }
}
方法名为:div 参数为:[2, 1]
方法结束
div方法结果为:2
---------------------------------
方法名为:add 参数为:[2, 5]
方法结束
add方法结果为:7
---------------------------------
方法名为:div 参数为:[2, 0]
方法结束
div方法抛出异常:java.lang.ArithmeticException: / by zero

java.lang.ArithmeticException: / by zero
。。。。。。(报错信息一大堆)

注意:

  • 可能会疑问comImpl我们都没有在xml中配置它,怎么能getBean获取?
    因为ComImpl使用了@Component注解,自动注入了IoC容器,而且前面我们知道,它的默认id就是类名的第一个字母小写后的名字;
  • 因为切面类中需要切入的是ComImpl类,所以这里getBean获取这个类的实例其实是获取它的代理类的实例; (⭐⭐)

4. AOP术语解释

在Spring AOP中,有如下几个术语:

1. Aspect(切面类)
  • 切面是一个模板,它定义了所有需要完成的工作,比如切入的范围和时间,都是在切面中来完成;
  • 在Spring中,通过实现@Aspect注解来构造一个切面;
  • 上面的例子中,LoggerAspect就是一个切面类;
2. Advice(通知)
  • 定义了切面是什么,何时使用,描述了切面要完成的工作,还解决何时执行这个工作的问题。
    • 其实通知简单地说就是切面类的代码,即非业务代码,上面例子中就是LoggerAspect中的代码;
  • 在切面的某个特定的连接点上执行的动作。其中包括了“around”、“before”和“after”等不同类型的通知。许多AOP框架(包括Spring)都是以拦截器做通知模型,并维护一个以连接点为中心的拦截器链。
3. Target(目标)
  • 被横切的对象,对应上面例子中的ComImpl类的实例化对象,将通知放入其中;
4. Proxy(代理)
  • 切面对象、通知、目标混合之后的内容,即我们用JDK动态代理机制创建的对象;
5. Join point(连接点)
  • 在Java程序执行中,我们可以把每个方法看成一个点,所有方法的执行就是这些点串联的结果;而连接点就是目标类需要被切入的的位置,即通知要插入业务代码的具体位置;
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值