主要内容:
1.静态代理回顾
2.接口创建对象的可行性分析
3.动态代理
4.InvocationHandler接口
5.小结
该博文非原创,大部分抄录自zhihu用户:bravo1988的原创文章。主要用于本人学习梳理
1.静态代理回顾
-
假设有一个需求,在项目现有所有类的方法前后打印日志。你如何在不修改已有代码的前提下,完成这个需求?
-
静态代理实现:
为现有的每一个类都编写一个对应的代理类,并且让代理类和目标类实现相同的接口。
代理对象内部维系着一个目标对象的引用属性,通过构造器传入一个目标对象实例,来初始化这个引用属性。然后在代理对象的方法内部调用目标对象的同名的这个方法,并在调用前后打印日志。也就是说,代理对象 = 增强代码 + 目标对象(原对象),有了代理对象后,就不用原对象了。
-
静态代理的缺陷:
每个代理类只能代理一个目标对象,所以要为每一个目标类或者每一个接口,都编写对应的代理类。如果当前系统有成千上万个类,那么工作量就太大了。所以,我们现在的努力方向就是:有没有抽取出来一个方法,传入什么对象,就能得到这个对象的代理对象。
2.接口创建实例对象的可行性分析
-
2.1.回想一下对象的创建过程
我们知道源代码经过javac命令编译后会在磁盘中得到字节码文件.class文件,也知道java命令会启动JVM将字节码文件加载进内存。但也仅仅止步于此了,之后从字节码文件加载进内存到堆中产生对象,期间具体发生了什么,并不清楚。
所谓“万物皆对象”,字节码文件也难逃“被对象”的命运。该类的.class字节码文件被加载进内存后,JVM也会为其创建了一个对象。以后所有该类的实例,皆以这个对象为模版,这个对象叫Class对象,它就是Class类的实例。
-
2.2.注意到在创建一个实例对象时,JVM会把.class文件加载进内存,然后创建一个Class类对象。
Class类是描述所有类信息的,而这个Class类对象,是描述某个特定对象(你想要实例的对象)的。该类的具体结构信息都会被映射到Class类对象中,包括方法,构造器。所以我们可以通过该类的Class类对象,来得到实例对象。
也就是说,要得到一个类的实例,关键是先得到该类的Class对象!只不过new这个关键字实在太方便了,为我们隐藏了很多底层细节。
-
2.3.那么怎么得到某该类的Class对象呢?
Class类的构造器是private的,杜绝了外界通过new创建Class对象的可能。当程序需要某个类时,JVM自己会调用这个构造器,并传入ClassLoader(类加载器),让它去加载字节码文件到内存,然后JVM为其创建对应的Class对象。
所以我看换种方式看看对象的创建过程;
所以关键一点:要得到一个类的实例,关键是要先得到该类的Class对象! -
2.4.看一下接口的Class类对象和类的Class类对象的区别:
1.来分析一下接口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());
}
}
}
2.运行结果:
3.区别
- 接口的Class对象没有构造方法,所以Calculator接口不能直接new对象
- 实现类Class对象有构造犯法,所以CalculatorImp实现类可以直接new对象
- 接口Class对象有两个自身的方法add()、subtract()
- 实现类Class对象除了add()、subtract(),还有从Object继承的方法。
那么两者的Class对象的主要区别就是:接口没有构造器。其他的结构都类似。也就是说,接口和对应的实现类的Class对象(信息)除了构造器,基本相似。
4.无论怎么说,我们都无法通过接口直接实例出一个对象。
即使考虑到接口的Class对象,还是没有构造器,所以无法通过接口直接实例出一个对象。
3.动态代理
回想一下我们想要什么?我们想要只通过一个接口,就能获得一个代理对象,来帮助我们增强某个类。
但是上面分析的结果是,接口的Class对象仍然没有构造器,所以我们无法通过接口直接实例出一个对象。
那动态代理是怎么创建实例的呢?他有没有类似通过一个接口得到一个代理对象实例的方法呢?
下面我们需要java.lang.reflect.invocationHandler接口和java.lang.reflect.Proxy类的支持。
-
3.1.通过查看API,我们发现Proxy类有一个静态方法可以帮助我们。
Proxy.getProxyClass():返回代理类的Class对象,参数是类加载器和一个接口的Class对象数组,得到一个代理类的Class类对线。也就是说,只要传入目标类实现的接口的Class对象,getProxyClass()方法就可以返回代理对象的Class类对象,而不用实际编写代理类。非常强大!
之后我们自然而然就可以用这个代理类的Class对象来实例创建一个代理对象。 -
3.2.注意一下和得到静态代理对象的区别:
-
代理对象要和目标对象有相似的结构,包括各种属性和方法等,所以每个类的对应的静态代理对象都会实现和这个类相同的接口。
public class CalculatorImpl implements Calculator {
}
-
如果是静态代理的话,
为什么每个类都要创建一个代理对象,就是因为每个类实现的接口情况是不同的,这个我们无法作为公共部分抽取出来。
-
如果我们想要抽取出来一个方法,这个方法能够为每个类都创建一个代理对象,那么这个方法就要解决每个目标类都有不同的接口实现这样情况。所以抽取的方式,就是将这个实现的接口作为参数来满足上述要求
(这个不太容易抽取出来的不同的部分,那么就抽取出来作为一个参数,因为每次传的参数可以是不同的)
。
public static Class<?> getProxyClass(ClassLoader loader,
Class<?>... interfaces)
-
3.3.Proxy.getProxyClass(ClassLoader loader, Class<?>… interfaces)
那么这个方法正好满足了我们上面分析的要求,将每个目标类实现的接口作为参数传递进来和一个类加载器,然后就能得到一个代理类的Class对象。
我们看一下这个代理类Class对象中都有什么信息:
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());
}
}
}
运行结果:
也就是说,通过Proxy.getProxyClass()传入类加载器和接口Class对象,我们得到了一个“加强版”的Class对象:即包含原接口的add(),subtract(),还包含一个构造器$Proxy0(InvocationHandler)
,还有一些自己特有的方法以及从Object继承的方法。
-
3.4.梳理一下
1.Class类中的newInstance()方法底层走的是类型参数T的无参构造
2.原先我们打算直接根据接口的Class对象得到代理对象,但最后发现接口的Class只有方法信息,没有构造器。
3.于是我们想,有没有一个办法,能创建出一个Class对象,既有接口Class的方法信息,同时又包含构造器方便创建代理实例呢?
4.最后找到Proxy类的静态方法getProxyClass()方法,给它传一个接口Class对象,就能返回一个代理类的Class对象,而且还有一个构造器。也就是说getProxyClass()的本质是:用(接口)Class对象,造(有参)的Class对象。
-
3.5.小结一下
getProxyClass()这个方法,会从你传入的接口的Class类对象数组中,“拷贝”类的结构信息放到一个新的Class对象,并返回。即根据传入的接口,得到代理类的Class对象。并且这个新的Class对象带有构造器,是可以创建对象的。这个构造器的参数是InvocationHandler接口的实现类。代理对象不需要实现接口,但是目标对象一定要实现接口,否则不能用动态代理。
-
3.6.创建一下代理实例
1.得到代理类的Class对象
2.这个代理类的Class对象中维系着一个有参构造对象,拿到这个有参构造对象
3.用这个有参构造对象实例出一个代理对象。
/**
* @param target
* 传目标对象,方便调原方法
* @return
*/
public static Object generateProxy(Object target) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
//1.得到代理类Proxy的Class类对象:有InvocationHandle属性。Proxy类中有这个属性
Class<?> proxyClass = Proxy.getProxyClass(target.getClass().getClassLoader(), target.getClass().getInterfaces());
//2.得到参数是InvocationHandle的构造器
Constructor<?> constructor = proxyClass.getConstructor(InvocationHandler.class);
//3. 根据构造器得到代理对象:有参构造
Object proxy = constructor.newInstance(new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//4.增强该类的所有方法
System.out.println("打印日志...");
Object result = method.invoke(target, args);
System.out.println("结束打印...");
return result;
}
});
return proxy;
}
4.InvocationHandler接口
- 1.上面说到Proxy.getProxyClass()得到的一个代理类的Class对象。而且这个Class对象中有一个有参构造方法对象。其中的参数是InvocationHandler接口。
- 2.InvocationHandler接口有一个invoke()方法,里面的参数是Object proxy:是代理对象本身,Method method:方法对象,Object[] args参数数组。代理对象调用任何方法都会被invoke拦截,所以后面代理对象想要实现什么额外的功能,都可以放到重写的invoke()方法里面。
public static Object generateProxy(Object target) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
//1.得到代理类Proxy的Class类对象:有InvocationHandle属性。Proxy类中有这个属性
Class<?> proxyClass = Proxy.getProxyClass(target.getClass().getClassLoader(), target.getClass().getInterfaces());
//2.得到参数是InvocationHandle的构造器
Constructor<?> constructor = proxyClass.getConstructor(InvocationHandler.class);
//3. 根据构造器得到代理对象:有参构造
Object proxy = constructor.newInstance(new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//4.增强该类的所有方法
System.out.println("打印日志...");
Object result = method.invoke(target, args);
System.out.println("结束打印...");
return result;
}
});
return proxy;
}
5.动态代理的缺陷
动态代理生成的代理对象,最终都可以用接口接收,和目标对象一起形成了多态。
动态代理的目标对象必须有实现的接口。
得到的代理独享只能使用或增强继承的接口中定义的方法。