Java代理

462 阅读29分钟

Java代理

在java中常见的代理有如下几种:

  • 静态代理

  • 基于JDK的动态代理

  • 基于CGlib的动态代理

  • Spring的动态代理

  • 其他形式的动态代理

基于JDK的代理类和被代理类,必须实现同一个接口。

基于Spring的动态代理(Spring+Aspectj)

Spring的动态代理是基于JDK的动态代理和CGlib的动态代理而成的。在Spring的动态代理中,有如下配置:

ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.setProxyTargetClass(true);
proxyFactory.setOptimize(true); 
proxyFactory.setFrozen(true);

setProxyTargetClass: 它表明是否代表目标类,默认值为false,意思是代理接口,设置为true后即代理类;

什么意思呢? 意思就是Spring的动态代理默认是基于JDK的动态代理代理接口,当设置proxyTargetClass为true后,即修改为基于CGlib的动态代理代理类。

setOptimize: 它表明对代理生成优化策略,默认值为false,意思是不启用。当设置为true时,代理对象有实现接口时,则使用JDK代理, 代理对象没有实现接口时,则使用CGlib代理。

setFrozen: 即在代理被配置之后,不允许修改代理的配置。

JDK代理只能代理有实现接口的被代理对象,而CGlib可以代理没有实现接口的被代理对象并且还可以代理有接口的被代理对象。这样比较下来,感觉CGlib代理好像百利而无一害,那么,直接用CGlib不就可以了,为啥还要使用JDK的动态代理呢?

CGlib创建代理的速度比较慢,但创建代理之后运行的速度却非常快,而JDK动态代理刚好相反。如果在运行的时候不断地用CGlib去创建代理,系统的性能会大打折扣,所以建议一般在系统初始化的时候用CGlib去创建代理,并放到Spring的ApplicationContext中以备后用。

一个小需求:给原有方法添加日志打印

假设现在我们有一个类Calculator,代表一个计算器,它可以进行加减乘除操作

public class Calculator {

	//加
	public int add(int a, int b) {
		int result = a + b;
		return result;
	}

	//减
	public int subtract(int a, int b) {
		int result = a - b;
		return result;
	}

	//乘法、除法...
}

现有一个需求:在每个方法执行前后打印日志。你有什么好的方案?

直接修改

很多人最直观的想法是直接修改Calculator类:

public class Calculator {

	//加
	public int add(int a, int b) {
		System.out.println("add方法开始...");
		int result = a + b;
		System.out.println("add方法结束...");
		return result;
	}

	//减
	public int subtract(int a, int b) {
		System.out.println("subtract方法开始...");
		int result = a - b;
		System.out.println("subtract方法结束...");
		return result;
	}

	//乘法、除法...
}

上面的方案是有问题的:

  1. 直接修改源程序,不符合开闭原则。应该对扩展开放,对修改关闭
  2. 如果Calculator有几十个、上百个方法,修改量太大
  3. 存在重复代码(都是在核心代码前后打印日志)
  4. 日志打印硬编码在代理类中,不利于后期维护:比如你花了一上午终于写完了,组长告诉你这个功能取消,于是你又要打开Calculator花十分钟删除日志打印的代码!

所以,此种方案PASS!

代理是一种模式,提供了对目标对象的间接访问方式,即通过代理访问目标对象。如此便于在目标实现的基础上增加额外的功能操作,前拦截,后拦截等,以满足自身的业务需求。

静态代理的实现比较简单:编写一个代理类,实现与目标对象相同的接口,并在内部维护一个目标对象的引用。通过构造器塞入目标对象,在代理对象中调用目标对象的同名方法,并添加前拦截,后拦截等所需的业务功能。

按上面的描述,代理类和目标类需要实现同一个接口,所以我打算这样做:

  • 将Calculator抽取为接口
  • 创建目标类CalculatorImpl实现Calculator
  • 创建代理类CalculatorProxy实现Calculator

接口

/**
 * Calculator接口
 */
public interface Calculator {
	int add(int a, int b);
	int subtract(int a, int b);
}

目标对象实现类

/**
 * 目标对象实现类,实现Calculator接口
 */
public class CalculatorImpl implements Calculator {

	//加
	public int add(int a, int b) {
		int result = a + b;
		return result;
	}

	//减
	public int subtract(int a, int b) {
		int result = a - b;
		return result;
	}

	//乘法、除法...
}

代理对象实现类

/**
 * 代理对象实现类,实现Calculator接口
 */
public class CalculatorProxy implements Calculator {
        //代理对象内部维护一个目标对象引用
	private Calculator target;
        
        //构造方法,传入目标对象
	public CalculatorProxy(Calculator target) {
		this.target = target;
	}

        //调用目标对象的add,并在前后打印日志
	@Override
	public int add(int a, int b) {
		System.out.println("add方法开始...");
		int result = target.add(a, b);
		System.out.println("add方法结束...");
		return result;
	}

        //调用目标对象的subtract,并在前后打印日志
	@Override
	public int subtract(int a, int b) {
		System.out.println("subtract方法开始...");
		int result = target.subtract(a, b);
		System.out.println("subtract方法结束...");
		return result;
	}

	//乘法、除法...
}

使用代理对象完成加减乘除,并且打印日志

public class Test {
	public static void main(String[] args) {
		//把目标对象通过构造器塞入代理对象
		Calculator calculator = new CalculatorProxy(new CalculatorImpl());
		//代理对象调用目标对象方法完成计算,并在前后打印日志
		calculator.add(1, 2);
		calculator.subtract(2, 1);
	}
}  

静态代理示意图

静态代理的优点:可以在不修改目标对象的前提下,对目标对象进行功能的扩展和拦截。但是它也仅仅解决了4大缺点中的第1点:

  1. 直接修改源程序,不符合开闭原则。应该对扩展开放,对修改关闭 √
  2. 如果Calculator有几十个、上百个方法,修改量太大 ×
  3. 存在重复代码(都是在核心代码前后打印日志) ×
  4. 日志打印硬编码在代理类中,不利于后期维护:比如你花了一上午终于写完了,组长告诉你这个功能取消,于是你又要打开Calculator花十分钟删除全部新增代码!×

静态代理的问题

上面案例中,代理类是我们事先编写的,而且要和目标对象类实现相同接口。由于CalculatorImpl(目标对象)需要日志功能,我们即编写了CalculatorProxy(代理对象),并通过构造器传入CalculatorImpl(目标对象),调用目标对象同名方法的同时添加增强代码。

但是这里有个问题!代理对象构造器的参数类型是Calculator,这意味着它只能接受Calculator的实现类对象,亦即我们写的代理类CalculatorProxy只能给Calculator做代理,它们绑定死了!

如果现在我们系统需要全面改造,给其他类也添加日志打印功能,就得为其他几百个接口都各自写一份代理类...

img

自己手动写一个类并实现接口实在太麻烦了。仔细一想,**我们其实想要的并不是代理类,而是代理对象!**那么,能否让JVM根据接口自动生成代理对象呢?

比如,有没有一个方法,我传入接口,它就给我自动返回代理对象呢?

img

答案是肯定的。


静态代理

具体做法如下:

1.为现有的每一个类都编写一个对应的代理类,并且让它实现和目标类相同的接口(假设都有)

imgimg

2.在创建代理对象时,通过构造器塞入一个目标对象,然后在代理对象的方法内部调用目标对象同名方法,并在调用前后打印日志。也就是说,代理对象 = 增强代码 + 目标对象(原对象),有了代理对象后,就不用原对象了

imgimg

静态代理的缺陷

程序员要手动为每一个目标类,编写对应的代理类。如果当前系统已经有成百上千个类,工作量太大了。所以,现在我们的努力方向是:如何少写或者不写代理类,却能完成代理功能?


接口创建对象的可行性分析

复习对象的创建过程

首先,在很多初学者的印象中,类和对象的关系是这样的:

img

虽然知道源代码经过javac命令编译后会在磁盘中得到字节码文件(.class文件),也知道java命令会启动JVM将字节码文件加载进内存,但也仅仅止步于此了。至于从字节码文件加载进内存到堆中产生对象,期间具体发生了什么,他们并不清楚。

所谓“万物皆对象”,字节码文件也难逃“被对象”的命运。它被加载进内存后,JVM为其创建了一个对象,以后所有该类的实例,皆以它为模板。这个对象叫Class对象,它是Class类的实例。

img

大家想想,Class类是用来描述所有类的,比如Person类,Student类...那我如何通过Class类创建Person类的Class对象呢?这样吗:

Class clazz = new Class();

好像不对吧,我说这是Student类的Class对象也行啊。有点晕了...

其实,程序员是无法自己new一个Class对象的,它仅由JVM创建。

img

  • Class类的构造器是private的,杜绝了外界通过new创建Class对象的可能。当程序需要某个类时,JVM自己会调用这个构造器,并传入ClassLoader(类加载器),让它去加载字节码文件到内存,然后JVM为其创建对应的Class对象
  • 为了方便区分,Class对象的表示法为:Class,Class

所以借此机会,我们不妨换种方式看待类和对象:

img

也就是说,**要得到一个类的实例,关键是先得到该类的Class对象!**只不过new这个关键字实在太方便,为我们隐藏了底层很多细节,我在刚开始学习Java时甚至没意识到Class对象的存在。

接口Class和类Class的区别

来分析一下接口Class和类Class的区别。以Calculator接口的Class对象和CalculatorImpl实现类的Class对象为例:

import java.lang.reflect.Constructor;
import java.lang.reflect.Executable;
import java.lang.reflect.Method;

public class ProxyTest {
	public static void main(String[] args) {
		/*Calculator接口的Class对象
                  得到Class对象的三种方式:1.Class.forName(xxx) 
                                           2.xxx.class 
                                           3.xxx.getClass()
                  注意,这并不是我们new了一个Class对象,而是让虚拟机加载并创建Class对象            
                */
		Class<Calculator> calculatorClazz = Calculator.class;
		//Calculator接口的构造器信息
		Constructor[] calculatorClazzConstructors = calculatorClazz.getConstructors();
		//Calculator接口的方法信息
		Method[] calculatorClazzMethods = calculatorClazz.getMethods();
		//打印
		System.out.println("------接口Class的构造器信息------");
		printClassInfo(calculatorClazzConstructors);
		System.out.println("------接口Class的方法信息------");
		printClassInfo(calculatorClazzMethods);

		//Calculator实现类的Class对象
		Class<CalculatorImpl> calculatorImplClazz = CalculatorImpl.class;
		//Calculator实现类的构造器信息
		Constructor<?>[] calculatorImplClazzConstructors = calculatorImplClazz.getConstructors();
		//Calculator实现类的方法信息
		Method[] calculatorImplClazzMethods = calculatorImplClazz.getMethods();
		//打印
		System.out.println("------实现类Class的构造器信息------");
		printClassInfo(calculatorImplClazzConstructors);
		System.out.println("------实现类Class的方法信息------");
		printClassInfo(calculatorImplClazzMethods);
	}

	public static void printClassInfo(Executable[] targets){
		for (Executable target : targets) {
			// 构造器/方法名称
			String name = target.getName();
			StringBuilder sBuilder = new StringBuilder(name);
			// 拼接左括号
			sBuilder.append('(');
			Class[] clazzParams = target.getParameterTypes();
			// 拼接参数
			for(Class clazzParam : clazzParams){
				sBuilder.append(clazzParam.getName()).append(',');
			}
			//删除最后一个参数的逗号
			if(clazzParams!=null && clazzParams.length != 0) {
				sBuilder.deleteCharAt(sBuilder.length()-1);
			}
			//拼接右括号
			sBuilder.append(')');
			//打印 构造器/方法
			System.out.println(sBuilder.toString());
		}
	}
}

运行结果:

img

  • 接口Class对象没有构造方法,所以Calculator接口不能直接new对象
  • 实现类Class对象有构造方法,所以CalculatorImpl实现类可以new对象
  • 接口Class对象有两个方法add()、subtract()
  • 实现类Class对象除了add()、subtract(),还有从Object继承的方法

也就是说,接口和实现类的Class信息除了构造器,基本相似。

既然我们希望通过接口创建实例,就无法避开下面两个问题:

1.接口方法体缺失问题

首先,接口的Class对象已经得到,它描述了方法信息。

但它没方法体。

没关系,反正代理对象的方法是个空壳,只要调用目标对象的方法即可。

JVM可以在创建代理对象时,随便糊弄一个空的方法体,反正后期我们会想办法把目标对象塞进去调用。

所以这个问题,勉强算是解决。

2.接口Class没有构造器,无法new

这个问题好像无解...毕竟这么多年了,的确没听哪位仁兄直接new接口的。

但是,仔细想想,接口之所以不能new,是因为它缺少构造器,它本身是具备完善的类结构信息的。就像一个武艺高强的大内太监(接口),他空有一身绝世神功(类结构信息),却后继无人。如果江湖上有一位妙手圣医,能克隆他的一身武艺,那么克隆人不就武艺高强的同时,还能生儿育女了吗? 所以我们就想,JDK有没有提供这么一个方法,比如getXxxClass(),我们传进一个接口Class对象,它帮我们克隆一个具有相同类结构信息,又具备构造器的新的Class对象呢?

至此,分析完毕,我们无法根据接口直接创建对象(废话)。

那动态代理是怎么创建实例的呢?它到底有没有类似getXxxClass()这样的方法呢?


动态代理

不错,动态代理确实存在getXxxClass()这样的方法。

我们需要java.lang.reflect.InvocationHandler接口和 java.lang.reflect.Proxy类的支持。Proxy后面会用到InvocationHandler,因此我打算以Proxy为切入点。首先,再次明确我们的思路:

img

通过查看API,我们发现Proxy类有一个静态方法可以帮助我们。

img

Proxy.getProxyClass():返回代理类的Class对象。终于找到妙手圣医。

也就说,只要传入目标类实现的接口的Class对象,getProxyClass()方法即可返回代理Class对象,而不用实际编写代理类。这相当于什么概念?

img

废话不多说,开搞。

import java.lang.reflect.Constructor;
import java.lang.reflect.Executable;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class ProxyTest {
	public static void main(String[] args) {
		/*
		 * 参数1:Calculator的类加载器(当初把Calculator加载进内存的类加载器)
		 * 参数2:代理对象需要和目标对象实现相同接口Calculator
		 * */
		Class calculatorProxyClazz = Proxy.getProxyClass(Calculator.class.getClassLoader(), Calculator.class);
		//以Calculator实现类的Class对象作对比,看看代理Class是什么类型
                System.out.println(CalculatorImpl.class.getName());
		System.out.println(calculatorProxyClazz.getName());
		//打印代理Class对象的构造器
		Constructor[] constructors = calculatorProxyClazz.getConstructors();
		System.out.println("----构造器----");
		printClassInfo(constructors);
		//打印代理Class对象的方法
		Method[] methods = calculatorProxyClazz.getMethods();
		System.out.println("----方法----");
		printClassInfo(methods);
	}

	public static void printClassInfo(Executable[] targets) {
		for (Executable target : targets) {
			// 构造器/方法名称
			String name = target.getName();
			StringBuilder sBuilder = new StringBuilder(name);
			// 拼接左括号
			sBuilder.append('(');
			Class[] clazzParams = target.getParameterTypes();
			// 拼接参数
			for (Class clazzParam : clazzParams) {
				sBuilder.append(clazzParam.getName()).append(',');
			}
			//删除最后一个参数的逗号
			if (clazzParams != null && clazzParams.length != 0) {
				sBuilder.deleteCharAt(sBuilder.length() - 1);
			}
			//拼接右括号
			sBuilder.append(')');
			//打印 构造器/方法
			System.out.println(sBuilder.toString());
		}
	}
}

运行结果:

img

大家还记得接口Class的打印信息吗?

img

也就是说,通过给Proxy.getProxyClass()传入类加载器和接口Class对象,我们得到了一个加强版的Class:即包含接口的方法信息add()、subtract(),又包含了构造器$Proxy0(InvocationHandler),还有一些自己特有的方法以及从Object继承的方法。

梳理一下:

1.原先我们本打算直接根据接口Class得到代理对象,无奈接口Class只有方法信息,没有构造器

2.于是,我们想,有没有办法创建一个Class对象,既有接口Class的方法信息,同时又包含构造器方便创建代理实例呢?

3.利用Proxy类的静态方法getProxyClass()方法,给它传一个接口Class对象,它能返回一个加强版Class对象。也就是说getProxyClass()的本质是:用Class,造Class。

img

要谢谢Proxy类和JVM,让我们不写代理类却直接得到代理Class对象,进而得到代理对象。

img静态代理

img动态代理:用Class造Class

既然Class<$Proxy0>有方法信息,又有构造器,我们试着用它得到代理实例吧:

img

我们发现,newInstance()创建对象失败。因为Class的newInstance()方法底层会走无参构造器。而之前打印Proxy0Class信息时,我们发现它没有无参构造,只有有参构造Proxy0的Class信息时,我们发现它没有无参构造,只有有参构造Proxy0(InvocationHandler)。那就靠它了:

imgconstructor.newInstance()需要传入一个InvocationHandler对象,这里采用匿名对象的方式,invoke()方法不做具体实现,直接返回null

舒服~


Proxy.getProxyClass()的秘密

一个小问题

好不容易通过Proxy.getProxyClass()得到代理Class,又通过反射最终得到代理对象,当然要玩一玩:

img

尴尬,竟然发生了空指针异常。纵观整个代码,新写的add()和subtract()返回值是int,不会是空指针。而再往上的代码之前编译都是通过的,应该没问题啊。再三思量,我们发现匿名对象InvocationHandler的invoke()返回null。难道是它?做个实验:让invoke()返回1,然后观察结果。

img结果代理对象的add和subtract都返回1

巧合吗?应该不是。我猜:**每次调用代理对象的方法都会调用invoke(),且invoke()的返回值就是代理方法的返回值。**如果真是如此,空指针异常就可以解释了:add()和suntract()期待的返回值类型是int,但是之前invoke()返回null,类型不匹配,于是空指针异常。

以防万一,再验证一下invoke()和代理对象方法的关系:

img

好了,什么都不用说了。就目前的实验来看,调用过程应该是这样:

img

动态代理底层调用逻辑

同样的,知道了结果后,我们再反推原理。

静态代理:往代理对象的构造器传入目标对象,然后代理对象调用目标对象的同名方法。

动态代理:constructor反射创建代理对象时,需要传入InvocationHandler,我猜,代理对象内部有一个成员变量InvocationHandler:

img

果然不出所料。那么动态代理的大致设计思路就是:

img

为什么这么设计?

为了解耦,也为了通用性。

如果JVM生成代理对象的同时生成了特定逻辑的方法体,那这个代理对象后期就没有扩展的余地,只能有一种玩法。而引入InvocationHandler的好处是:

  • JVM创建代理对象时不必考虑方法实现,只要造一个空壳的代理对象,舒服
  • 后期代理对象想要什么样的方法实现,我写在invocationHandler对象的invoke()方法里送进来便是

所以,invocationHandler的作用,倒像是把“方法”和“方法体”分离。JVM只造一个空的代理对象给你,后面想怎么玩,由你自己组装。反正代理对象中有个成员变量invocationHandler,每一个方法里只有一句话:handler.invoke()。所以调任何一个代理方法,最终都会跑去调用invoke()方法。

invoke()方法是代理对象和目标对象的桥梁。

img

但是我们真正想要的结果是:调用代理对象的方法时,去调用目标对象的方法。

所以,接下来努力的方向就是:设法在invoke()方法得到目标对象,并调用目标对象的同名方法。

代理对象调用目标对象方法

那么,如何在invoke()方法内部得到目标对象呢?我们来看看能不能从invoke()方法的形参上获取点线索:

  • Object proxy:很遗憾,是代理对象本身,而不是目标对象(不要调用,会无限递归)
  • Method method:本次被调用的代理对象的方法
  • Obeject[] args:本次被调用的代理对象的方法参数

很可惜,proxy不是代理对象。其实想想也知道,创建代理对象的过程中自始至终没有目标对象参与,所以也就无法产生关联。而且一个接口可以同时被多个类实现,所以JVM也无法判断当前代理对象想要代理哪个目标对象。但好在我们已经知道本次调的方法名(Method)和参数(args)。我们接下来要做的就是得到目标对象并调用同名方法,然后把参数给它。

如何得到目标对象呢?没办法,为今之计只能new了...哈哈哈哈。我靠,饶了一大圈,又是动态代理,又是invoke()的,结果还是要手动new?别急,先玩玩。后面会改进的:

img

但是这样的写法显然是倒退30年,一夜回到解放前。我们需要改进一下,封装Proxy.getProxyClass(),使得目标对象可以作为参数传入:

public class ProxyTest {
	public static void main(String[] args) throws Throwable {
		CalculatorImpl target = new CalculatorImpl();
                //传入目标对象
                //目的:1.根据它实现的接口生成代理对象 2.代理对象调用目标对象方法
		Calculator calculatorProxy = (Calculator) getProxy(target);
		calculatorProxy.add(1, 2);
		calculatorProxy.subtract(2, 1);
	}

	private static Object getProxy(final Object target) throws Exception {
		//参数1:随便找个类加载器给它, 参数2:目标对象实现的接口,让代理对象实现相同接口
		Class proxyClazz = Proxy.getProxyClass(target.getClass().getClassLoader(), target.getClass().getInterfaces());
		Constructor constructor = proxyClazz.getConstructor(InvocationHandler.class);
		Object proxy = constructor.newInstance(new InvocationHandler() {
			@Override
			public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
				System.out.println(method.getName() + "方法开始执行...");
				Object result = method.invoke(target, args);
				System.out.println(result);
				System.out.println(method.getName() + "方法执行结束...");
				return result;
			}
		});
		return proxy;
	}
}

img

厉害厉害...可惜,还是太麻烦了。有没有更简单的方式获取代理对象?有!

img直接返回代理对象,而不是代理对象Class

从一开始就存在,哈哈。但是我觉得getProxyClass()切入更好理解。

public class ProxyTest {
	public static void main(String[] args) throws Throwable {
		CalculatorImpl target = new CalculatorImpl();
		Calculator calculatorProxy = (Calculator) getProxy(target);
		calculatorProxy.add(1, 2);
		calculatorProxy.subtract(2, 1);
	}

	private static Object getProxy(final Object target) throws Exception {
		Object proxy = Proxy.newProxyInstance(
				target.getClass().getClassLoader(),/*类加载器*/
				target.getClass().getInterfaces(),/*让代理对象和目标对象实现相同接口*/
				new InvocationHandler(){/*代理对象的方法最终都会被JVM导向它的invoke方法*/
					public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
						System.out.println(method.getName() + "方法开始执行...");
						Object result = method.invoke(target, args);
						System.out.println(result);
						System.out.println(method.getName() + "方法执行结束...");
						return result;
					}
				}
		);
		return proxy;
	}
}

编写可生成代理和可插入通知的通用方法

上面的代码,已经比上一篇开头直接修改目标类好多了。再来看一下当时的四大缺点:

  1. 直接修改源程序,不符合开闭原则。应该对扩展开放,对修改关闭√
  2. 如果Calculator有几十个、上百个方法,修改量太大√
  3. 存在重复代码(都是在核心代码前后打印日志)×
  4. 日志打印硬编码在代理类中,不利于后期维护:比如你花了一上午终于写完了,组长告诉你这个功能取消,于是你又要打开Calculator花十分钟删除日志打印的代码!×

使用动态代理,让我们避免手写代理类,只要给getProxy()方法传入target就可以生成对应的代理对象。但是日志打印仍是硬编码在invoke()方法中。虽然修改时只要改一处,但是别忘了“开闭原则”。所以最好是能把日志打印单独拆出来,像目标对象一样作为参数传入。

日志打印其实就是AOP里的通知概念。我打算定义一个Advice接口,并且写一个MyLogger实现该接口。

通知接口

public interface Advice {
	void beforeMethod(Method method);
	void afterMethod(Method method);
}

日志打印

public class MyLogger implements Advice {

	public void beforeMethod(Method method) {
		System.out.println(method.getName() + "方法执行开始...");
	}

	public void afterMethod(Method method) {
		System.out.println(method.getName() + "方法执行结束...");
	}
}

测试类

public class ProxyTest {
	public static void main(String[] args) throws Throwable {
		CalculatorImpl target = new CalculatorImpl();
		Calculator calculatorProxy = (Calculator) getProxy(target, new MyLogger());
		calculatorProxy.add(1, 2);
		calculatorProxy.subtract(2, 1);
	}

	private static Object getProxy(final Object target, Advice logger) throws Exception {
		/*代理对象的方法最终都会被JVM导向它的invoke方法*/
		Object proxy = Proxy.newProxyInstance(
				target.getClass().getClassLoader(),/*类加载器*/
				target.getClass().getInterfaces(),/*让代理对象和目标对象实现相同接口*/
				(proxy1, method, args) -> {
					logger.beforeMethod(method);
					Object result = method.invoke(target, args);
					System.out.println(result);
					logger.afterMethod(method);
					return result;
				}
		);
		return proxy;
	}
}

差一点完美~下篇讲讲更完美的做法。


类加载器补充

初学者可能对诸如“字节码文件”、Class对象比较陌生。所以这里花一点点篇幅介绍一下类加载器的部分原理。如果我们要定义类加载器,需要继承ClassLoader类,并覆盖findClass()方法:

@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
	try {
		/*自己另外写一个getClassData()
                  通过IO流从指定位置读取xxx.class文件得到字节数组*/
		byte[] datas = getClassData(name);
		if(datas == null) {
			throw new ClassNotFoundException("类没有找到:" + name);
		}
		//调用类加载器本身的defineClass()方法,由字节码得到Class对象
		return this.defineClass(name, datas, 0, datas.length);
	} catch (IOException e) {
		e.printStackTrace();
		throw new ClassNotFoundException("类找不到:" + name);
	}
}

所以,这就是类加载之所以能把xxx.class文件加载进内存,并创建对应Class对象的深层原因。具体文章可以参考基友写的另一篇:请叫我程序猿大人:好怕怕的类加载器


小结

静态代理

代理类CalculatorProxy是我们事先写好的,编译后得到Proxy.class字节码文件。随后和目标类一起被ClassLoader(类加载器)加载进内存,生成Class对象,最后生成实例对象。代理对象中有目标对象的引用,调用同名方法并前后加上日志打印。

img

优点:不用修改目标类源码

缺点是:高度绑定,不通用。硬编码,不易于维护。

动态代理

我们本想通过接口Class直接创建代理实例,无奈的是,接口Class虽然有方法信息描述,却没有构造器,无法创建对象。所以我们希望JDK能提供一套API,我们传入接口Class,它自动复制里面的方法信息,造出一个有构造器、能创建实例的代理Class对象。

img

优点:

  • 不用写代理类,根据目标对象直接生成代理对象
  • 通知可以传入,不是硬编码

彩蛋

上面的讨论都在刻意回避代理对象的类型,放最后来聊一聊。

最后讨论一下代理对象是什么类型。

首先,请区分两个概念:代理Class对象和代理对象。

img

单从名字看,代理Class和Calculator的接口确实相去甚远,但是我们却能讲代理对象赋值给接口类型:

img

但谁说能否复制给接口是看名字的?难道不是只要实现接口就行了吗?

代理对象的本质就是:和目标对象实现相同接口的实例。代理Class可以叫任何名字,whatever,只要它实现某个接口,就能成为该接口类型。

img

我写了一个MyProxy类,那么它的Class名字必然叫MyProxy。**但这和能否赋值给接口没有任何关系。**由于它实现了Serializable和Collection,所以myProxy(代理实例)同时是这两个接口的类型。

我想了个很骚的比喻,希望能解释清楚:

接口Class对象是大内太监,里面的方法和字段比做他的一身武艺,但是他没有小DD(构造器),所以不能new实例。一身武艺后继无人。

那怎么办呢?

正常途径(implements):

写一个类,实现该接口。这个就相当于大街上拉了一个人,认他做干爹。一身武艺传给他,只是比他干爹多了小DD,可以new实例。

非正常途径(动态代理):

通过妙手圣医Proxy的克隆大法(Proxy.getProxyClass()),克隆一个Class,但是有小DD。所以这个克隆人Class可以创建实例,也就是代理对象。

代理Class其实就是附有构造器的接口Class,一样的类结构信息,却能创建实例。

imgJDK动态代理生成的实例

imgCGLib动态代理生成的实例

如果说继承的父类是亲爹(只有一个),那么实现的接口是干爹(可以有多个)。

实现接口是一个类认干爹的过程。接口无法创建对象,但实现该接口的类可以。

比如

class Student extends Person implements A, B

这个类new一个实例出来,你问它:你爸爸是谁啊?它会告诉你:我只有一个爸爸Person。

但是student instanceof A interface,或者student instanceof B interface,它会告诉你两个都是它干爹(true),都可以用来接收它。

img

然而,凡是有利必有弊。

img

也就是说,动态代理生成的代理对象,最终都可以用接口接收,和目标对象一起形成了多态,可以随意切换展示不同的功能。但是切换的同时,只能使用该接口定义的方法。


介绍完JDK动态代理,今天和大家一起做个小案例:模拟Spring的事务管理。

主要内容:

  • 熟悉的陌生人
  • 山寨AOP事务需求分析
  • AOP事务具体代码实现

熟悉的陌生人

面试官如果问“请你谈谈你对Spring的理解”,估计很多人会脱口而出:IOC和AOP。IOC大概是大家对Spring最直接的印象,就是个大容器,装了很多bean,还会帮你做依赖注入。

imgIOC

但是对于AOP,很多人其实没有太多概念,一时不知道Spring哪里用了AOP。好像事务用了切面,但具体又不了解。这样吧,我问你一个问题,我自己写了一个UserController,以及UserServiceImpl implements UserService,并且在UserController中注入Service层对象:

@Autowired
private UserService userService;

那么,这个userService一定是我们写的UserServiceImpl的实例吗?

如果你听不懂我要问什么,说明你本身对Spring的了解还是太局限于IOC。

实际上,Spring依赖注入的对象并不一定是我们自己写的类的实例,也可能是userServiceImpl的代理对象。下面分别演示这两种情况:

  • 注入userServiceImpl对象

img注入的是UserServiceImpl类型

  • 注入userServiceImpl的代理对象(CGLib动态代理)

img注入的是CGLib动态代理生成的userServiceImpl的代理对象

为什么两次注入的对象不同?

因为第二次我给UserServiceImpl加了@Transactional 注解。

img

此时Spring读取到这个注解,便知道我们要使用事务。而我们编写的UserService类中并没有包含任何事务相关的代码。如果给你,你会怎么做?动态代理嘛!

但是要用动态代理完成事务管理,还需要自己编写一个通知类,并把通知对象传入代理对象,通知负责事务的开启和提交,并在代理对象内部调用目标对象同名方法完成业务功能。

img

我们能想到的方案,Spring肯定也知道。同样地,Spring为了实现事务,也编写了一个通知类,TransactionManager。利用动态代理创建代理对象时,Spring会把transactionManager织入代理对象,然后将代理对象注入到UserController。

所以我们在UserController中使用的userService其实是代理对象,而代理对象才支持事务。


山寨AOP事务需求分析

了解了Spring事务的大致流程后,我们再来分析一下自己如何编写一个山寨的AOP事务。

AOP事务,有两个概念:AOP和事务。

事务,大家已经很熟悉,这里主要讲讲什么是AOP。AOP,它是**Aspect-Oriented Programming(面向切面编程)**的英文缩写。什么是面向切面编程?有时直接介绍一个东西是什么,可能比较难。但是一说到它是干嘛的,大家就立即心领神会了。

我们的系统中,常常存在交叉业务,比如事务、日志等。UserService的method1要用到它,BrandService的method2也要用到它。一个交叉业务就是要切入系统的一个方面。具体用代码展示就是:

这个切面,可以是日志,也可以是事务

​ 这个切面,可以是日志,也可以是事务

交叉业务的编程问题即为面向切面编程。AOP的目标就是使交叉业务模块化。可以将切面代码移动到原始方法的周围:

img

原先不用AOP时,交叉业务直接写在方法内部的前后,用了AOP交叉业务写在方法调用前后。这与AOP的底层实现方式有关:动态代理其实就是代理对象调用目标对象的同名方法,并在调用前后加增强代码。不过这两种最终运行效果是一样的。

而所谓的模块化,我个人的理解是将切面代码做成一个可管理的状态。比如日志打印,不再是直接硬编码在方法中的零散语句,而是做成一个通知类,通过通知去执行切面代码。

所以,现在需求已经很明确,我们需要一个通知类(TransactionManager)执行事务,一个代理工厂帮助生成代理对象,然后利用动态代理将事务代码织入代理对象的各个方法中。

就好比下面三个Service,原先是没有开启事务的:

img

我们希望最终达到的效果是,我加了个@MyTransactional后,代理工厂给我返回一个代理对象:

img

代理工厂使用动态代理,为每一个目标对象创建一个代理对象

细节分析:

imgtxManager其实是在目标对象test()方法的前后执行事务,而不是方法内部的前后

也就是说,代理对象方法 = 事务 + 目标对象方法。

另外,还有个棘手的问题:事务操作,必须使用同一个Connection对象。如何保证?第一次从数据源获取Connection对象并开启事务后,将它存入当前线程的ThreadLocal中,等到了DAO层,还是从ThreadLocal中取,这样就能保证开启事务和操作数据库使用的Connection对象是同一个。

img

img

img开启事务后,Controller并不是直接调用我们自己写的Service,而是Spring提供的代理对象

这就是事务的实现原理。


AOP事务具体代码实现

ConnectionUtils工具类

package com.demo.myaopframework.utils;

import org.apache.commons.dbcp.BasicDataSource;

import java.sql.Connection;

/**
 * 连接的工具类,它用于从数据源中获取一个连接,并且实现和线程的绑定
 */
public class ConnectionUtils {

    private ThreadLocal<Connection> tl = new ThreadLocal<Connection>();

    private static BasicDataSource dataSource = new BasicDataSource();

    //静态代码块,设置连接数据库的参数
    static{
        dataSource.setDriverClassName("com.mysql.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/test");
        dataSource.setUsername("root");
        dataSource.setPassword("123456");
    }


    /**
     * 获取当前线程上的连接
     * @return
     */
    public Connection getThreadConnection() {
        try{
            //1.先从ThreadLocal上获取
            Connection conn = tl.get();
            //2.判断当前线程上是否有连接
            if (conn == null) {
                //3.从数据源中获取一个连接,并且存入ThreadLocal中
                conn = dataSource.getConnection();
                tl.set(conn);
            }
            //4.返回当前线程上的连接
            return conn;
        }catch (Exception e){
            throw new RuntimeException(e);
        }
    }

    /**
     * 把连接和线程解绑
     */
    public void removeConnection(){
        tl.remove();
    }
}

AOP通知(事务管理器)

package com.demo.myaopframework.utils;

/**
 * 和事务管理相关的工具类,它包含了,开启事务,提交事务,回滚事务和释放连接
 */
public class TransactionManager {

    private ConnectionUtils connectionUtils;

    public void setConnectionUtils(ConnectionUtils connectionUtils) {
        this.connectionUtils = connectionUtils;
    }

    /**
     * 开启事务
     */
    public  void beginTransaction(){
        try {
            connectionUtils.getThreadConnection().setAutoCommit(false);
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    /**
     * 提交事务
     */
    public  void commit(){
        try {
            connectionUtils.getThreadConnection().commit();
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    /**
     * 回滚事务
     */
    public  void rollback(){
        try {
            connectionUtils.getThreadConnection().rollback();
        }catch (Exception e){
            e.printStackTrace();
        }
    }


    /**
     * 释放连接
     */
    public  void release(){
        try {
            connectionUtils.getThreadConnection().close();//还回连接池中
            connectionUtils.removeConnection();
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

自定义注解

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyTransactional {
}

Service

public interface UserService {
	void getUser();
}

 
public class UserServiceImpl implements UserService {
	@Override
	public void getUser() {
		System.out.println("service执行...");
	}
}

实例工厂

public class BeanFactory {

	public Object getBean(String name) throws Exception {
		//得到目标类的Class对象
		Class<?> clazz = Class.forName(name);
		//得到目标对象
		Object bean = clazz.newInstance();
		//得到目标类上的@MyTransactional注解
		MyTransactional myTransactional = clazz.getAnnotation(MyTransactional.class);
		//如果打了@MyTransactional注解,返回代理对象,否则返回目标对象
		if (null != myTransactional) {
			ProxyFactoryBean proxyFactoryBean = new ProxyFactoryBean();
			TransactionManager txManager = new TransactionManager();
			txManager.setConnectionUtils(new ConnectionUtils());
			//装配通知和目标对象
			proxyFactoryBean.setTxManager(txManager);
			proxyFactoryBean.setTarget(bean);
			Object proxyBean = proxyFactoryBean.getProxy();
			//返回代理对象
			return proxyBean;
		}
		//返回目标对象
		return bean;
	}
}

代理工厂

public class ProxyFactoryBean {
	//通知
	private TransactionManager txManager;
	//目标对象
	private Object target;

	public void setTxManager(TransactionManager txManager) {
		this.txManager = txManager;
	}

	public void setTarget(Object target) {
		this.target = target;
	}

	//传入目标对象target,为它装配好通知,返回代理对象
	public Object getProxy() {
		Object proxy = Proxy.newProxyInstance(
				target.getClass().getClassLoader(),/*1.类加载器*/
				target.getClass().getInterfaces(), /*2.目标对象实现的接口*/
				new InvocationHandler() {/*3.InvocationHandler*/
					@Override
					public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
						try {
							//1.开启事务
							txManager.beginTransaction();
							//2.执行操作
							Object retVal = method.invoke(target, args);
							//3.提交事务
							txManager.commit();
							//4.返回结果
							return retVal;
						} catch (Exception e) {
							//5.回滚事务
							txManager.rollback();
							throw new RuntimeException(e);
						} finally {
							//6.释放连接
							txManager.release();
						}

					}
				}
		);
		return proxy;
	}

}

代码结构

img

得到普通UserService:

img

给UserServiceImpl添加@MyTransactional注解,得到代理对象:

img

img

参考资料:

  • 传智播客张孝祥 Java高新技术