Spring AOP学习(三)

Wesley13
• 阅读 753

前面已经对Spring AOP做了个简单介绍,今天来分析一下Spring AOP的原理 -- JDK和Cglib代理。

Spring AOP的原理分为三部分,概述、设计模式和实现,见下图:

Spring AOP学习(三)

1、原理概述:织入的时机分为三种,分别是:

1)编译期(AspectJ)

2)类加载时(AspectJ 5+)

3)运行时(Spring AOP )

        运行时织入是怎么实现的?通过代理对象来实现的,代理分为动态代理和静态代理,其中基于动态代理的实现分为两种,一种是基于接口代理与基于继承代码实现,接下来会重点分析这两种实现方式。

2、设计模式

1)Spring AOP中用到了责任链模式和代理模式,Spring AOP中的责任链模式主要是应用在AOP的链式调用上。

2)代理模式

(1)代理模式定义:代理(Proxy)是一种设计模式,提供了对目标对象另外的访问方式;即通过代理对象访问目标对象.这样做的好处是:可以在目标对象实现的基础上,增强额外的功能操作,即扩展目标对象的功能.

(2)代理模式作用:为其他对象提供一种代理以便控制对这个对象的访问。

(3)代理分类:代理模式分为两种,见下图:

Spring AOP学习(三)

        静态代码缺点:要代理的方法越多,代码重复率越高。假设你的目标类有100个方法,那么你的代理类里面就要对这100个方法进行委托,但代理类里面每个方法执行的前后代码都是一致的,所以代码重复性高。

        动态代理的两类实现:基于接口代理和基于继承代理(两个的代表分别是JDK代理和Cglib代理)。

代理模式一般涉及到的角色有:

  • 抽象角色:声明真实对象和代理对象的共同接口; 
  • 代理角色:代理对象角色内部含有对真实对象的引用,从而可以操作真实对象,同时代理对象提供与真实对象相同的接口以便在任何时刻都能代替真实对象。同时,代理对象可以在执行真实对象操作时,附加其他的操作,相当于对真实对象进行封装。 
  • 真实角色:代理角色所代表的真实对象,是我们最终要引用的对象。

下面是代理模式的类图:

Spring AOP学习(三)

类图说明:这个类图中有两个部分需要注意的:

(1)由于目标对象中有的方法在代理模式中都要有,所以目标类和代理类实现了同一个接口,这体现了面向对象编程的面向接口编程。

(2)代理类中引用了真实目标对象,AOP就是在代理类在调用真实目标对象之前或之后或其它时机做了些额外的工作(比如打印日志等非功能性的功能)。也就是说代理对象把真正的方法委托给目标对象去执行,而自己就去执行一些额外的逻辑,就是Aop要织入的代码,从而实现切面切入。

3、实现:

1)JDK代理的实现:

(1)实现要点:

  • 类:java.lang.reflect.Proxy  -- 通过Proxy类动态生成代理类。
  • 接口:InvocationHandler   -- 代理类要实现织入逻辑,需要实现这个接口。
  • 注意只能基于接口进行动态代理

 

(2)下面通过有一个例子来说明JDK代理

①定义一个普通类:

/**
 * 普通接口 
 */
public interface Subject {
    void request();
    void hello();
}

②定义一个实现接口的类:

/**
 *    真实目标类 
 */
public class RealSubject implements Subject{

    @Override
    public void request() {
        System.out.println("RealSubject calls the method of request now ……");
    }

    @Override
    public void hello() {
        System.out.println("RealSubject calls the method of hello now ……");
    }

}

③ 自定义InvocationHandler,通过反射机制调用目标对象中的方法:

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

import cn.exercise.patten.proxy.RealSubject;

/**
 *    自定义InvocationHandler
 */
public class JdkProxySubject implements InvocationHandler{
    
    private RealSubject realSubject; // 目标对象
    
    public JdkProxySubject(RealSubject realSubject) {
        this.realSubject = realSubject;
    }
    
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("--- jdkProxy before ---");
        Object result = null;
        try {
            result = method.invoke(realSubject, args); // 利用反射调用目标对象的方法
        } catch (Exception e) {
            System.out.println("ex: " + e.getMessage());
        } finally {
            System.out.println("--- jdkProxy after ---");
            System.out.println();
        }
        return result;
    }

}

        invoke方法参数说明:

  • Object proxy生成的代理对象,在这里不是特别的理解这个对象,但是个人认为是已经在内存中生成的proxy对象。
  • Method method:被代理的对象中被代理的方法的一个抽象。
  • Object[] args:被代理方法中的参数。这里因为参数个数不定,所以用一个对象数组来表示。

④编写客户端类:

import java.lang.reflect.Proxy;

import cn.exercise.patten.proxy.RealSubject;
import cn.exercise.patten.proxy.Subject;
/**
 * 客户端类
 */
public class Client {

    public static void main(String[] args) {
        Subject subject = (Subject) Proxy.newProxyInstance(Client.class.getClassLoader(), // 目标对象通过getClass方法获取类的所有信息后,调用getClassLoader()方法来获取类加载器。获取类加载器后,可以通过这个类型的加载器,在程序运行时,将生成的代理类加载到JVM中,以便运行时需要!
                new Class[]{Subject.class}, //获取被代理类的一组口信息,以便于生成的代理类可以具有代理类接口中的所有方法。
                new JdkProxySubject(new RealSubject()));//自定义InvocationHandler
        subject.request();
        subject.hello();
    }    

}

⑤运行结果:

Spring AOP学习(三)

(3)JDK代理源码分析

        接下来我们看看Proxy.newProxyInstance()这个方法里面的部分代码,下图是JDK源码的一个时序图,通过这个时序图的找到apply()方法,查看里面的一段重要代码:

Spring AOP学习(三)

        说明:这段代码的作用是验证传入的类加载器载入的Class和你传入的接口对应的Class是否相同,不相等则抛出异常。红框框出的部分是通过反射机制,加载每一个接口的运行时Class信息,通过接口的名称,找到类,在接着一步往下执行,生成字节码,最后生成代理类。

    通过System.setProperties()可以设置保存jdk动态代理生成的字节码文件。

    我们通过反编译,来看看通过这种方式生成的代理类是怎么样的:

//生成的代理类
public final class $Proxy0 extends Proxy implements Subject
{
  private static Method m1;
  private static Method m3;
  private static Method m2;
  private static Method m4;
  private static Method m0;
  
  public $Proxy0(InvocationHandler paramInvocationHandler)
    throws 
  {
    super(paramInvocationHandler);
  }
  
  public final boolean equals(Object paramObject)
    throws 
  {
    try
    {
      return ((Boolean)this.h.invoke(this, m1, new Object[] { paramObject })).booleanValue();
    }
    catch (Error|RuntimeException localError)
    {
      throw localError;
    }
    catch (Throwable localThrowable)
    {
      throw new UndeclaredThrowableException(localThrowable);
    }
  }
  
  public final void hello()
    throws 
  {
    try
    {
      this.h.invoke(this, m3, null);
      return;
    }
    catch (Error|RuntimeException localError)
    {
      throw localError;
    }
    catch (Throwable localThrowable)
    {
      throw new UndeclaredThrowableException(localThrowable);
    }
  }
  
  public final String toString()
    throws 
  {
    try
    {
      return (String)this.h.invoke(this, m2, null);
    }
    catch (Error|RuntimeException localError)
    {
      throw localError;
    }
    catch (Throwable localThrowable)
    {
      throw new UndeclaredThrowableException(localThrowable);
    }
  }
  
  public final void request()
    throws 
  {
    try
    {
      this.h.invoke(this, m4, null);
      return;
    }
    catch (Error|RuntimeException localError)
    {
      throw localError;
    }
    catch (Throwable localThrowable)
    {
      throw new UndeclaredThrowableException(localThrowable);
    }
  }
  
  public final int hashCode()
    throws 
  {
    try
    {
      return ((Integer)this.h.invoke(this, m0, null)).intValue();
    }
    catch (Error|RuntimeException localError)
    {
      throw localError;
    }
    catch (Throwable localThrowable)
    {
      throw new UndeclaredThrowableException(localThrowable);
    }
  }
  
  static
  {
    try
    {
      m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[] { Class.forName("java.lang.Object") });
      m3 = Class.forName("com.imooc.pattern.Subject").getMethod("hello", new Class[0]);
      m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
      m4 = Class.forName("com.imooc.pattern.Subject").getMethod("request", new Class[0]);
      m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
      return;
    }
    catch (NoSuchMethodException localNoSuchMethodException)
    {
      throw new NoSuchMethodError(localNoSuchMethodException.getMessage());
    }
    catch (ClassNotFoundException localClassNotFoundException)
    {
      throw new NoClassDefFoundError(localClassNotFoundException.getMessage());
    }
  }
}

    由上面的图可以看到这种方式是基于接口动态生成代理类的,通过自定义的InvocationHandler里面的invoke()方法来获取目标类方法的信息。

2)Cglib代理

        JDK实现动态代理需要实现类通过接口定义业务方法,对于没有接口的类,如何实现动态代理呢,这就需要CGLib了。CGLib采用了非常底层的字节码技术,其原理是通过字节码技术为一个类创建子类(通过ASM字节码处理框架实现),并在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑。JDK动态代理与CGLib动态代理均是实现Spring AOP的基础。

        JDK动态代理的拦截对象是通过反射的机制来调用被拦截方法的,反射的效率比较低,所以Cglib采用了FastClass的机制来实现对被拦截方法的调用。FastClass机制就是对一个类的方法建立索引,通过索引来直接调用相应的方法。

(1)Cglib实现要点

  • Cglib是通过继承的方式时实现代理类
  • 通过Enhancer类的setCallback()方法织入代码

    

(2)接下来通过一个例子来说明Cglib的使用

①创建真实目标对象类:

/**
 *    真实目标类 
 */
public class RealSubject implements Subject{

    @Override
    public void request() {
        System.out.println("RealSubject calls the method of request now ……");
    }

    @Override
    public void hello() {
        System.out.println("RealSubject calls the method of hello now ……");
    }

}

②创建织入代码类:

import java.lang.reflect.Method;

import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

/**
 * 织入代码类
 */
public class DemoMethodInterceptor implements MethodInterceptor{

    @Override
    public Object intercept(Object obj, Method arg1, Object[] args, MethodProxy proxy) throws Throwable {
        System.out.println("--- before ---");
        Object result = null;
        try {
            result = proxy.invokeSuper(obj, args);
        } catch (Exception e) {
            System.out.println("ex:" + e.getMessage());
        } finally {
            System.out.println("--- after ---");
        }
        return result;
    }

}

        说明:Enhancer类是CGLib中的一个字节码增强器,它可以方便的对你想要处理的类进行扩展 。MethodInterception是Cglib的接口,通过实现这个接口接口,在intercept中织入想要的非功能性代码。

③创建客户端类:

import cn.exercise.patten.proxy.RealSubject;
import cn.exercise.patten.proxy.Subject;
import net.sf.cglib.proxy.Enhancer;
/**
 * 客户端
 */
public class Client {
    public static void main(String[] args) {
        Enhancer enhancer = new Enhancer(); // 传入目标对象,通过继承生成代理类
        enhancer.setSuperclass(RealSubject.class); // 织入代码
        enhancer.setCallback(new DemoMethodInterceptor());
        Subject subject = (Subject) enhancer.create();
        
        //调用方法
        subject.hello();
        subject.request();
    }
}

④运行结果

Spring AOP学习(三)

3)JDK与Cglib对比

(1)JDK只能针对有接口的类的接口方法进行动态代理;

(2)Cglib基于继承来实现代理,无法对static、final类进行代理;(单一的类是没有static修饰符的,只有静态类内部可以用static修饰;)

(3)Cglib基于继承来实现代理,无法对private、static方法进行代理。(对于static方法,它是属于类的,子类在不重写的情况下,是可以调用的,但是一旦重写了就无法调用了,普通的public方法可以通过super.method()调用,但是static方法不行。)

4)看到这里,或许你的问题就来了:既然有JDK代理,又有Cglib代理,那Spring AOP怎样判断使用JDK代理还是Cglib代理的?

        这就涉及Spring的源码了,我们通过下面的spring时序图,找到DefaultAopProxyFactory类,查看里面的代码。时序图如下:

Spring AOP学习(三)

        说明:

  • AbstractAutoProxyCreator:spring的代理创建类都是AbstractAutoProxyCreator的子类,这个抽象类同时又是InstantiationAwareBeanPostProcessor的实现类。 (1)wrapIfNecessary:包给bean如果必要,即如果它是合格的代理。 (2)createProxy(bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean))

  • ProxyFactory:AOP代理程序使用的工厂,而不是通过声明式安装在一个bean工厂。这个类提供了一种简单的方式获取和用户自定义代码配置AOP代理实例。

  • ProxyCreatorSupport:代理工厂基类。提供方便的访问一个可配置的aopproxyfactory。

  • DefaultAopProxyFactory:这个类中的部分代码现实了SpringAOP是如何选择代理方式的。见下面的源码:

    @Override public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException { if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) { Class<?> targetClass = config.getTargetClass(); if (targetClass == null) { throw new AopConfigException("TargetSource cannot determine target class: " + "Either an interface or a target is required for proxy creation."); } if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) { return new JdkDynamicAopProxy(config); } return new ObjenesisCglibAopProxy(config); } else { return new JdkDynamicAopProxy(config); } }

        由上面的源码,总结SpringAOP是如何选择代理方式:

(1)如果目标对象实现了接口,则默认采用JDK动态代理;

(2)如果目标对象没有实现接口,则采用Cglib进行动态代理;

(3)如果目标对象实现了接口,且强制Cglib代理,则使用cglib代理;

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Stella981 Stella981
3年前
KVM调整cpu和内存
一.修改kvm虚拟机的配置1、virsheditcentos7找到“memory”和“vcpu”标签,将<namecentos7</name<uuid2220a6d1a36a4fbb8523e078b3dfe795</uuid
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
Java日期时间API系列36
  十二时辰,古代劳动人民把一昼夜划分成十二个时段,每一个时段叫一个时辰。二十四小时和十二时辰对照表:时辰时间24时制子时深夜11:00凌晨01:0023:0001:00丑时上午01:00上午03:0001:0003:00寅时上午03:00上午0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
10个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这