欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

spring专题---第二部分AOP

程序员文章站 2024-03-16 10:40:28
...

       前面的内容集中对spring IOC进行了详细讲解。spring的两大核心机制是IOC和AOP,本节我们一起总结spring AOP的相关知识。
       AOP(Aspect Oriented Programming)意为面向切面编程,我们可能比较熟悉的是OOP(面向对象编程),其实AOP就是OOP的一个延申。它是在另外一个维度上抽象出对象,具体是指程序运行时动态地将非业务代码切入到业务代码中,从而实现代码的解耦合,将非业务代码抽象成一个对象,对该对象进行编程这就是面向切面编程思想
下面以一幅经典的图来简单说明一下AOP
spring专题---第二部分AOP
       AOP的有点:
       •可大大降低模块之间的耦合性
       •提高代码的维护性
       •提高代码的复用性
       •集中管理非业务代码,便于维护
       •业务代码不受非业务代码的影响,逻辑更加清晰
       说概念比较空泛,不好理解。我们还是通过代码来直观感受什么是AOP。
(1)创建一个计算器接口Cal,定义四个方法:加、减、乘、除。

//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)创建Cal接口的实现类,CalImpl.java

//CalImpl.java
public class CalImpl implements Cal{
    public int add(int num1, int num2) {
        int result=num1+num2;
        System.out.println("add:"+result);
        return result;
    }

    public int sub(int num1, int num2) {
        int result=num1-num2;
        System.out.println("sub:"+result);
        return result;
    }

    public int mul(int num1, int num2) {
        int result=num1*num2;
        System.out.println("mul:"+result);
        return result;
    }

    public int div(int num1, int num2) {
        int result=num1/num2;
        System.out.println("div:"+result);
        return result;
    }
}

       对于我这里为什么要创建一个接口和实现类而不是直接创建一个实现类,原因有二,一是Spring AOP中的动态代理主要有两种方式,JDK动态代理和CGLIB动态代理。而JDK动态代理需要目标类的接口,如果没有接口,是无法完成的。二是现在的项目大多提倡面向接口编程,有了接口,能使我们代码更规范,多人协作也更方便。
(3)创建测试类Test.java

Test.java
public class Test {
    public static void main(String[] args) {
        Cal cal=new CalImpl();
        cal.add(10,2);
        cal.sub(10,2);
        cal.mul(10,2);
        cal.div(10,2);
    }
}

运行结果:
spring专题---第二部分AOP
       好了,我们看到正式我们想要的结果,那么现在问题来了,假如我需求变了,我想要让它们不仅输出结果,还要输出是哪两个数在进行运算,这个时候相信大家觉得不难,在添加些代码嘛,好的,我们继续撸代码。

//修改CalImpl.java,添加了一部分非业务性的代码
public class CalImpl implements Cal{
    public int add(int num1, int num2) {
        int result=num1+num2;
        System.out.println(num1+" add "+num2);
        System.out.println("result="+result);
        return 0;
    }

    public int sub(int num1, int num2) {
        int result=num1-num2;
        System.out.println(num1+" sub "+num2);
        System.out.println("result="+result);
        return 0;
    }

    public int mul(int num1, int num2) {
        int result=num1*num2;
        System.out.println(num1+" mul "+num2);
        System.out.println("result="+result);
        return 0;
    }

    public int div(int num1, int num2) {
        int result=num1/num2;
        System.out.println(num1+" div "+num2);
        System.out.println("result="+result);
        return 0;
    }
}

运行结果:
spring专题---第二部分AOP
       我们看到我们同样也实现了想要的功能,但是我们思考一下,如果我现在需求又变了,我不想要哪两个数运算这部分代码了,我是不是还要进入实现类中一个方法一个方法的删除,这个时候我们想一下,业务少的时候我们还能顶得住,如果成千上百个呢,光想一想就觉得是一件耗费精力还意义不大的一件事情,由此便出现了AOP这个奇妙的东西,它的出现就是为了解决这个问题。
我们可以换个角度去分析,会发现4个方法中打印日志信息的代码基本相同,那么有没有可能将这部门代码提取出来进行封装,统一维护呢?同时也可以将日志代码和业务代码完全分离开,实现解耦合。
       按照这个思路继续向下走,我们希望做的事情是把这4个方法的相同位置(业务方法执行前后)提取出来,形成一个横切面,并且将这个横切面抽象成一个对象,将所有的打印日志代码写到这个对象中,以实现与业务代码的分离。
       这就是AOP的思想
       如何实现?使用动态代理的方式来实现。
       我们希望CalImpl只进行业务运算,不进行打印日志工作,那么就需要有一个对象来替代CalImpl进行打印日志工作,这就是代理对象
       具体实现方式如下:
(1)删除CalImpl方法中所有打印日志的代码,只保留业务代码。

//CalImpl.java
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接口,成为一个动态代理类。

//MyInvocationHandler.java  这里的InvocationHandler接口不是我们手写的,是引入java.lang.reflect包下。
public class MyInvocationHandler implements InvocationHandler {

    private Object obj=null;

    public Object bind(Object obj){
        this.obj=obj;
        return Proxy.newProxyInstance(obj.getClass().getClassLoader(),obj.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(this.obj,args);
        System.out.println(method.getName()+"的结果是:"+result);
        return result;
    }
}

(3)测试代码:

//Test.java
public class Test {
    public static void main(String[] args) {
        Cal cal=new CalImpl();
        MyInvocationHandler mh=new MyInvocationHandler();
        Cal cal2=(Cal)mh.bind(cal);
        cal2.add(10,2);
        cal2.sub(10,2);
        cal2.mul(10,2);
        cal2.div(10,2);
    }
}

运行结果:
spring专题---第二部分AOP
       bind方法是MyInvocationHandler类提供给外部调用的方法,传入委托对象,bind方法会返回一个代理对象,bind方法完成了两项工作:
(1)将外部传进来的委托对象保存到成员变量中,因为业务方法调用时需要用到委托对象。
(2)通过Proxy.newProxyInstance方法创建一个代理对象,解释一下Proxy.newProxyInstance方法的参数:
       •我们知道对象是JVM根据运行时类来创建的,此时需要动态创建一个代理对象的运行时类,同时需要将这个动态创建的运行时类加载到JVM中,这一步需要获取到类加载器才能实现,我们可以通过委托对象的运行时类来反向获取类加载器,obj.getClass().getClassLoader()就是通过委托对象的运行时类来获取类加载器的具体实现;
       •同时代理对象需要具备委托对象的所有功能,即需要拥有委托对象的所有接口,因此传入obj.getClass.getInterfaces();
       •this指当前MyInvocationHandler对象。
以上全部是反射的知识点。
       •invoke方法:method是描述委托对象所有方法的对象,args是描述委托对象方法参数列表的对象。
       method.invoke(this.obj,args)是通过反射机制来调用委托对象的方法,即业务方法。
       因此在method.iinvoke(this.obj,args)前后添加打印日志信息,就等同于在委托对象业务代码中添加打印日志信息,并且已经做到了分类,业务方法在委托对象中,打印日志信息在代理对象中。
       以上就是通过动态代理实现AOP的过程,我们在使用spring框架AOP时,并不需要这么复杂,因为spring对这个过程已经进行了封装,让开发者可以更加便捷地使用AOP进行开发。
       接下来我们一起学习spring框架的AOP如何使用。
       在spring框架中,我们不需要创建MyInvocationHandler类,只需要创建一个切面类,spring底层会自动根据切面类以及目标类生成一个代理对象
(1)创建切面类LoggerAspect.java

//LoggerAspect.java
@Aspect
@Component
public class LoggerAspect {

    @Before("execution(public int 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 CalImpl.*(..))")
    public void after(JoinPoint joinPoint){
        String name=joinPoint.getSignature().getName();
        System.out.println(name+"方法结束");
    }

    @AfterReturning(value = "execution(public int 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 CalImpl.*(..))",throwing = "ex")
    public void afterThrowing(JoinPoint joinPoint,Exception ex){
        String name=joinPoint.getSignature().getName();
        System.out.println(name+"方法抛出异常:"+ex);
    }
}

       LoggerAspect类名处添加两个注解:
       @Aspect,表示该类是切面类;
       @Component,将该类注入到IOC容器中。
       分别来说明类中的4个方法注解的含义。
       •@Before:表示before方法执行的时机。execution(public int XXX.CalImpl.*(…)):表示切入点是XXX包下CalImpl类中的所有方法。即CalImpl所有方法在执行之前都会首先执行LoggerAspect类中的before方法。
       •after方法同理,表示CalImpl所有方法执行之后会执行LoggerAspect类中的after方法。
       •afterReturn方法表示CalImpl所有方法在return之后会执行LoggerAspect类中的afterReturn方法。
       •afterThrowing方法表示CalImpl所有方法在抛出异常时会执行LoggerAspect类中的afterThrowing方法。
       因此我们就可以根据具体需求,选择在before,after,afterReturn,afterThrowing方法中添加相应代码。
(2)目标类中也需要添加@Component注解,如下所示:

//CalImpl.java
@Component
public class CalImpl implements Cal {

    @Override
    public int add(int num1, int num2) {
        return num1+num2;
    }

    @Override
    public int sub(int num1, int num2) {
        return num1-num2;
    }

    @Override
    public int mul(int num1, int num2) {
        return num1*num2;
    }

    @Override
    public int div(int num1, int num2) {
        return num1/num2;
    }
}

(3)在spring.xml中进行配置

    <!--spring.xml-->>
    <!--自动扫描-->>
    <context:component-scan base-package="XXX.XX.X"/>
    <!--使Aspect注解生效,为目标类自动生成代理对象-->>
    <aop:aspectj-autoproxy/>

       •将XXX.XX.X包中的类扫描到IOC容器中。
       •添加aop:aspectj-autoproxy注解,spring容器会结合切面类和目标类自动生成动态代理对象,spring框架的AOP底层就是通过动态代理的方式完成AOP。
(4)测试方法执行如下,从IOC容器中获取代理对象。

//Test.java
public class Test {
    public static void main(String[] args) {
        ApplicationContext applicationContext=new ClassPathXmlApplicationContext("spring-biz.xml");
        Cal cal=(Cal)applicationContext.getBean("loggerAspect");
        cal.add(10,2);
        cal.sub(10,2);
        cal.mul(10,2);
        cal.div(10,2);
    }
}

       下面咱们根据上边例子再来理解一下概念
       •切面对象:根据切面对象出来的对象,即CalImpl所有方法中需要加入日志部分,抽象成一个切面对象LoggerAspect。
       •通知:切面对象的具体代码,即非业务代码,LoggerAspect对象打印日志的操作。
       •目标:被横切的对象,即CalImpl实例化对象,将通知加入其中。
       •代理:切面对象、通知、目标混合之后的内容,即我们用JDK动态代理机制创建的对象。
       •连接点:需要被横切的位置,即通知要插入业务代码的具体位置。
       如果,你对于AOP的概念还是不清楚,那么我将之前看过的一篇文章总结一下,那篇作者生动的解释了AOP,相信对你或许有些帮助。
       spring用代理类包裹切面,把他们织入到Spring管理的bean中。也就是说代理类伪装成目标类,它会截取对目标类中方法的调用,让调用者对目标类的调用都先变成调用伪装类,伪装类中就先执行了切面,再把调用转发给真正的目标bean。
       现在可以自己想一想,怎么搞出来这个伪装类,才不会被调用者发现(过JVM的检查,JAVA是强类型检查,哪里都要检查类型)。
       1.实现和目标类相同的接口,我也实现和你一样的接口,反正上层都是接口级别的调用,这样我就伪装成了和目标类一样的类(实现了同一接口,咱是兄弟了),也就逃过了类型检查,到java运行期的时候,利用多态的后期绑定(所以spring采用运行时),伪装类(代理类)就变成了接口的真正实现,而他里面包裹了真实的那个目标类,最后实现具体功能的还是目标类,只不过伪装类在之前干了点事情(写日志,安全检查,事物等)。
       这就好比,一个人让你办件事,每次这个时候,你弟弟就会先出来,当然他分不出来了,以为是你,你这个弟弟虽然办不了这事,但是他知道你能办,所以就答应下来了,并且收了点礼物(写日志),收完礼物了,给把事给人家办了啊,所以你弟弟又找你这个哥哥来了,最后把这是办了的还是你自己。但是你自己并不知道你弟弟已经收礼物了,你只是专心把这件事情做好。
       顺着这个思路想,要是本身这个类就没实现一个接口呢,你怎么伪装我,我就压根没有机会让你搞出这个双胞胎的弟弟,那么就用第2种代理方式,创建一个目标类的子类,生个儿子,让儿子伪装我
       2.生成子类调用,这次用子类来做为伪装类,当然这样也能逃过JVM的强类型检查,我继承的吗,当然查不出来了,子类重写了目标类的所有方法,当然在这些重写的方法中,不仅实现了目标类的功能,还在这些功能之前,实现了一些其他的(写日志,安全检查,事物等)。
       这次的对比就是,儿子先从爸爸那把本事都学会了,所有人都找儿子办事情,但是儿子每次办和爸爸同样的事之前,都要收点小礼物(写日志),然后才去办真正的事。当然爸爸是不知道儿子这么干的了。这里就有件事情要说,某些本事是爸爸独有的(final的),儿子学不了,学不了就办不了这件事,办不了这个事情,自然就不能收人家礼了。
       前一种兄弟模式,spring会使用JDK的java.lang.reflect.Proxy类,它允许Spring动态生成一个新类来实现必要的接口,织入通知,并且把对这些接口的任何调用都转发到目标类。
       后一种父子模式,spring使用CGLIB库生成目标类的一个子类,在创建这个子类的时候,spring织入通知,并且把对这个子类的调用委托到目标类。
       相比之下,还是兄弟模式好些,他能更好的实现松耦合,尤其在今天都高喊着面向接口编程的情况下,父子模式只是在没有实现接口的时候,也能织入通知,应当做一种例外。
推荐一个关于AOP的博客
       •以上便是我对这一部分的理解,如果有错误或者你有其他疑惑都可以留言给出,我都会一一进行回复,希望对你有所帮助,如果写的不好也请您多多包涵。欢迎在下方补充留言哟。
        •对SSM框架感兴趣的童鞋,可以移步这里,在这里你可以快速的搭建好一个SSM框架。
       •如果你在写项目的时候,遇到一些不寻常的问题,也可以关注我的博客园,上边会发布一些我在写项目中遇到的各种问题以及解决方式。

相关标签: spring