天天看点

Spring —— AOP原理(JDK、cglib动态代理)、为什么JDK动态代理是面向接口?动态代理

在我另一篇博客SSM之Spring 02 —— 动态代理、AOP、Spring-MyBatis、声明式事务中有简单描述代理模式和AOP原理,当时自己也还是有些没明白,后面在面试过程中被问到,这里详细说明一下。

AOP是什么就不再说明了,主要解释下AOP的原理——动态代理。

动态代理

和静态代理不同,动态代理是动态生成代理对象,克服了静态代理的问题——不同目标对象都需要写一个代理类。

动态代理主要有两种方式:JDK的动态代理和cglib代理。

JDK的动态代理

它是对对象进行代理,底层利用反射机制实现,同时需要我们自定义一个实现了InvocationHandler接口的类,这个接口中的invoke方法就是AOP的本质。

下面我们通过代码来进行理解。

1、定义好UserDao以及实现类,这是我们常见的方式。

//为了方便就不写参数了
public interface UserDao {
    boolean addUser();
    boolean deleteUser();
}

public class UserDaoImpl implements UserDao {
    @Override
    public boolean addUser() {
        System.out.println("addUser();");
        return false;
    }

    @Override
    public boolean deleteUser() {
        System.out.println("deleteUser();");
        return true;
    }
}
           

2、写一个测试,具体解释看注解。可以看到Proxy和InvocationHandler都是reflect包下的,说明底层肯定是反射。

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;

public class Test {
    public static void main(String[] args) {
      	//创建好目标对象
        UserDao ud = new UserDaoImpl();
      
      	//这个就是我们自定义的一个实现类,里面invoke方法就是AOP的原理。
      	InvocationHandler ih = new MyInvocationHandler(ud);//将ud作为目标对象传进去
      
        /*
        		这个方法返回一个代理对象,我们会在代理对象中做一些其他操作
            newProxyInstance(ClassLoader, interfaces, InvocationHandler)
            1、ClassLoader:目标对象的类加载器,代码是固定的。
            2、interfaces:目标对象的接口数组,代码也是固定的,都是通过反射获得。
            3、InvocationHandler:这是关键点,AOP能实现方法的增强就在这个接口里。需要自己定义。
         */
        Object newProxyInstance = Proxy.newProxyInstance(ud.getClass().getClassLoader(),
                ud.getClass().getInterfaces(), ih);
      
        //执行代理对象被增强后的方法(增强的意思是在原有方法的基础上添加了其他效果,同时不影响原有业务,同Spring AOP的事务)
        UserDao proxyUserDao = (UserDao)newProxyInstance;
        proxyUserDao.addUser();
        System.out.println("--------------");
        proxyUserDao.deleteUser();
    }
}
           

3、自定义的实现类,这是实现AOP增强方法的关键。

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

public class MyInvocationHandler implements InvocationHandler {
    private Object object;

    public MyInvocationHandler(Object object) {
        this.object = object;
    }

    /**
     *
     * @param proxy 代理对象,就是我们从外部传进来的,但一般不用这个,会用我们自定义的object成员。
     * @param method 目标对象的方法对象,是JDK传过来的。
     * @param args 目标对象方法的参数对象。
     * @return 对应于我们目标对象的方法的返回值
     * @throws Throwable
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        String before = "";
        String after = "";
        //这里通过反射来判断UserDao的两个方法,如果不判断,那么就会针对目标对象的所有方法进行增强。
        if(method.getName().equals("addUser")){
            before = "前置通知:addUser()方法执行 之前";
            after = "后置通知:addUser()方法执行 之后";
        }
        if(method.getName().equals("deleteUser")){
            before = "前置通知:deleteUser()方法执行 之前";
            after = "后置通知:deleteUser()方法执行 之后";
        }
        //下面这样就实现了和Spring AOP 一样的效果:方法的增强,即在方法执行前做一些其他事,且不影响到原有业务。
        System.out.println(before);
        Object result = method.invoke(object, args);//通过反射调用目标对象的方法
        System.out.println(after);
        return result;
    }
}
           

4、运行结果:

Spring —— AOP原理(JDK、cglib动态代理)、为什么JDK动态代理是面向接口?动态代理

为什么说JDK是面向接口的?

这里的接口不是指InvocationHandler,而是指UserDao,我们看看通过

newProxyInstance

得带的代理类的父类。

Spring —— AOP原理(JDK、cglib动态代理)、为什么JDK动态代理是面向接口?动态代理

显然,只要我们通过JDK的Proxy代理获得的类,都会继承Proxy,那么我们是如何能够让这个类有UserDaoImpl的方法的呢?因为这个类继承了UserDao。

Spring —— AOP原理(JDK、cglib动态代理)、为什么JDK动态代理是面向接口?动态代理

这也就解释了,为什么我们说JDK动态代理是面向接口的。因为Java是单继承,而我们通过JDK动态代理得到的代理都会继承Proxy,同时我们要表明这个代理对象是UserDaoImpl的实例,那么就只能通过接口的方式。

因此,如果我们有实现类(例如UserDaoImpl),就可以考虑到JDK动态代理。这也是Spring AOP的默认实现方式,我们一般都是用实现类。

cglib

和JDK动态代理不同,cglib(code generator library 代码生成库)代理的是字节码对象(即Class),而JDK动态代理代理的是对象。

Spring —— AOP原理(JDK、cglib动态代理)、为什么JDK动态代理是面向接口?动态代理

步骤:

  1. cglib生成一个字节码对象(即Class对象),内容是空的。
  2. 设置字节码对象的父类为目标对象,即UserDaoImpl。
  3. 通过字节码对象中继承了目标对象的方法,即addUser,进行回调,调用父类的addUser,并在执行其方法前完成方法增强。
  4. 创建代理对象,通过代理对象执行Class的方法。

注意:cglib是Spring中的,所以需要导入Spring核心包才能使用。

Spring —— AOP原理(JDK、cglib动态代理)、为什么JDK动态代理是面向接口?动态代理

导入进来

Spring —— AOP原理(JDK、cglib动态代理)、为什么JDK动态代理是面向接口?动态代理

下面进行代码说明:

这里面CallBack就是AOP的原理,它通过子类调用父类UserDaoImpl的addUser方法,并在调用时完成增强。

package test02_AOP.cglib;

import org.springframework.cglib.proxy.Callback;
import org.springframework.cglib.proxy.Enhancer;

public class Test {
    public static void main(String[] args) {
        Enhancer enhancer = new Enhancer();//空的字节码对象
        enhancer.setSuperclass(UserDaoImpl.class);//设置父类
        Callback callback = new MyCallback();
        enhancer.setCallback(callback);//设置回调

        UserDao userDao = (UserDao) enhancer.create();//用代理类Enhancer 创建代理对象
        userDao.addUser();
    }
}
           
package test02_AOP.cglib;

import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

//本来是继承Callback接口,表示回调
//但Callback接口里什么都没有,只是起到一个标志作用,所以继承其子类MethodInterceptor
//MethodInterceptor接口仅有一个方法,拦截我们待增强的方法
public class MyCallback implements MethodInterceptor {
    /**
     *
     * @param proxy 代理对象,即前面图里面的 class XX extends UserDaoImpl的XX对象
     * @param method 目标对象的方法
     * @param args 目标对象中的 方法对象 的参数对象(相当于addUser方法的参数)
     * @param methodProxy 代理对象中的 方法代理对象(相当于父类UserDaoImpl的addUser方法)
     * @return 返回代理对象方法的结果
     * @throws Throwable
     */
    @Override
    public Object intercept(
            Object proxy, Method method, Object[] args, MethodProxy methodProxy
                           ) throws Throwable {
        System.out.println("前置通知");
        //执行原来的方法
        Object object = methodProxy.invokeSuper(proxy, args);//这里不能用invoke,相当于前面图里super.addUser()
        System.out.println("后置通知");

        return object;
    }
}
           

结果:

Spring —— AOP原理(JDK、cglib动态代理)、为什么JDK动态代理是面向接口?动态代理

总结

AOP在Spring中实现:

1、注解方式

  • 有一个注解@EnableAspectJAutoProxy(ProxyTargetClass = false)
  • 默认为false。如果是实现类(实现),就会用jdk,如果没有实现接口 ,就会用 cglib。
  • 如果是true。无论是否实现接口,都会使用 cglib。
    Spring —— AOP原理(JDK、cglib动态代理)、为什么JDK动态代理是面向接口?动态代理
Spring —— AOP原理(JDK、cglib动态代理)、为什么JDK动态代理是面向接口?动态代理

2、XML方式

3、JDK和cglib小结

  • JDK动态代理是面向接口的。
  • cglib是通过字节码底层继承被代理类,然后重写父类方法(如果被代理类继承final则会失败)
  • 如果被代理的对象是一个实现类(例如UserDaoImpl),那么Spring AOP会默认用JDK动态代理,否则使用cglib。
  • 性能比较:(参考博客 https://blog.csdn.net/xlgen157387/article/details/82497594)
    • 关于两者之间的性能的话,JDK动态代理所创建的代理对象,在以前的JDK版本中,性能并不是很高,虽然在高版本中JDK动态代理对象的性能得到了很大的提升,但是他也并不是适用于所有的场景。主要体现在如下的两个指标中:

      1、CGLib所创建的动态代理对象在实际运行时候的性能要比JDK动态代理高不少,有研究表明,大概要高10倍;

      2、但是CGLib在创建对象的时候所花费的时间却比JDK动态代理要多很多,有研究表明,大概有8倍的差距;

      3、因此,对于singleton的代理对象或者具有实例池的代理,因为无需频繁的创建代理对象,所以比较适合采用CGLib动态代理,反正,则比较适用JDK动态代理。

    • 经过原博主的实验后得出结果:(具体请参考原博客)

      最终的测试结果大致是这样的,在1.6和1.7的时候,JDK动态代理的速度要比CGLib动态代理的速度要慢,但是并没有教科书上的10倍差距,在JDK1.8的时候,JDK动态代理的速度已经比CGLib动态代理的速度快很多了。

    • 原博主下的一个评论(曾经的随性的回复)

      jdk的版本优化中主要是针对虚拟机对反射调用的优化,在jdk1.6中,我们采用JDK代理的方式来生成动态代理类,反射方法的调用在15次以内是调用本地方法,即是java到c++代码转换的方法,这种方式比直接生成字节码文件要快的多,而在15次之后则开始使用java实现的方式。而在1.8的版本优化中,反射调用的次数达到阈值[也就是发射调用的类成为热点时]之后采用字节码的方式,因为字节码的 方式只有在第一次生成字节码文件时比较消耗时间。