java
proxy
因為最近一段時間準備將這幾年做的一些業(yè)務(wù)和技術(shù)做個沉淀阔馋,也自己造的一些輪子呕寝,發(fā)現(xiàn)時不時就會需要用到動態(tài)代理和反射,所以今天打算先對jdk
的動態(tài)代理這部分內(nèi)容做個簡單的整理
介紹
先來說說怎么理解動態(tài)代理吧
首先在java
中有一種模式叫代理模式,代理的定義是
代理是指以他人的名義塞蹭,在授權(quán)范圍內(nèi)進行對被代理人直接發(fā)生法律效力的法律行為
對應(yīng)到代理模式
- 以他人名義 > 代理對象
- 授權(quán)范圍內(nèi) > 基于接口或類的規(guī)范限制(如無法代理接口或類中不存在的方法等)或是可實現(xiàn)的功能范圍(如無法獲得被代理方法的中間變量等)
- 被代理人 > 被代理的對象
- 法律 > java中的規(guī)范(如訪問限制等)
- 直接發(fā)生法律效力 > 直接影響方法執(zhí)行邏輯(如在前后打印日志或是修改入?yún)⒎祷刂捣纾瑢Ψ椒ǖ淖罱K執(zhí)行結(jié)果產(chǎn)生了影響)
- 法律行為 > 及代理本身的行為(符合java規(guī)范)
所以代理模式就是在通過代理對象調(diào)用被代理對象的方法過程中改變原有方法的執(zhí)行邏輯
好了辆琅,完全被自己繞暈了婉烟,其實代理能做到的功能完全可以枚舉出來
- 修改入?yún)?/li>
- 修改返回值
- 異常處理
- 日志打印
- 完全重寫
一般也就用到這些功能
而所謂的動態(tài)代理育瓜,就是這個代理對象是通過代碼動態(tài)生成的叉存,也就是用代碼生成代碼扬霜,經(jīng)典套娃了屬于是
使用場景
對于動態(tài)代理的使用場景绒尊,主要還是用于一些大型框架中
其中一個場景就是
Spring
利用動態(tài)代理實現(xiàn)強大的切面功能另一個場景就是
Feign
在接口上添加注解來實現(xiàn)HTTP
的調(diào)用婴谱,或是MyBatis
在接口上添加注解來執(zhí)行SQL
等等
方式
主流的動態(tài)代理有2種:jdk
動態(tài)代理和cglib
動態(tài)代理
說說這兩種方式的區(qū)別
-
jdk
動態(tài)代理只能代理接口(統(tǒng)一繼承Proxy
類)躯泰,cglib
動態(tài)代理可以代理接口也可以代理類(通過生成子類,所以不能被final
修飾) -
jdk
動態(tài)代理直接寫Class
文件瘟裸,cglib
借助ASM
框架處理Class
文件 -
jdk
動態(tài)代理方法內(nèi)調(diào)用其他方法無法被代理(通過某個實現(xiàn)類調(diào)用方法時)话告,cglib
動態(tài)代理方法內(nèi)調(diào)用的其他方法也可以被代理
接下來詳細聊聊jdk
動態(tài)代理吧卵慰,由于篇幅原因cglib
動態(tài)代理考慮另開一篇
JDK動態(tài)代理
基于jdk8
用法
用法就簡單過一下吧
public interface JdkDynamicProxy {
String methodToProxy();
}
JdkDynamicProxy target = new JdkDynamicProxy() {
@Override
public String methodToProxy() {
return "Target method to proxy";
}
};
JdkDynamicProxy proxy = (JdkDynamicProxy) Proxy
.newProxyInstance(JdkDynamicProxy.class.getClassLoader(),
new Class[]{JdkDynamicProxy.class},
new InvocationHandler() {
@Override
public Object invoke(Object proxy,
Method method,
Object[] args)
throws Throwable {
System.out.println(method.getName());
return method.invoke(target, args);
}
});
System.out.println(proxy.methodToProxy());
用法很簡單裳朋,調(diào)用Proxy.newProxyInstance
方法就能生成一個代理對象,然后就可以在被代理對象的方法調(diào)用的前后做一些有限的事情
參數(shù)
接下來說說這個方法的參數(shù)
先說第二個參數(shù)送挑,傳入一個類型為Class<?>[]
,名稱為interfaces
的對象纺裁,也就是你要代理的接口司澎,因為java
可以實現(xiàn)多個接口惭缰,所以是一個數(shù)組
再來說一下第三個參數(shù)InvocationHandler
,這是一個接口络凿,當你通過代理對象調(diào)用接口的方法時昂羡,就會回調(diào)該接口的invoke
方法并回傳代理對象虐先,方法和方法入?yún)?/p>
最后說第一個參數(shù),需要傳入一個ClassLoader
撰洗,一般情況下你用這個項目里面任何一個能得到的類加載器都沒問題腐芍,這個類加載器的主要作用就是校驗其可以訪問指定的接口以及加載這個代理類
//省略部分代碼
private static final class ProxyClassFactory
implements BiFunction<ClassLoader, Class<?>[], Class<?>> {
@Override
public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) {
//校驗類加載器可以訪問指定的接口
for (Class<?> intf : interfaces) {
/*
* Verify that the class loader resolves the name of this
* interface to the same Class object.
*/
Class<?> interfaceClass = null;
try {
interfaceClass = Class.forName(intf.getName(), false, loader);
} catch (ClassNotFoundException e) {
}
if (interfaceClass != intf) {
throw new IllegalArgumentException(
intf + " is not visible from class loader");
}
}
//使用類加載器定義類
try {
return defineClass0(loader, proxyName,
proxyClassFile, 0, proxyClassFile.length);
} catch (ClassFormatError e) {
/*
* A ClassFormatError here means that (barring bugs in the
* proxy class generation code) there was some other
* invalid aspect of the arguments supplied to the proxy
* class creation (such as virtual machine limitations
* exceeded).
*/
throw new IllegalArgumentException(e.toString());
}
}
}
流程
接下來就說說生成代理對象的整個流程
克隆接口數(shù)組
final Class<?>[] intfs = interfaces.clone();
首先會對我們傳入的第二個參數(shù)猪勇,也就是接口數(shù)組進行拷貝
這里我猜想可能是防止數(shù)組中的元素在生成代理的過程中被修改導(dǎo)致出現(xiàn)一些問題泣刹,不得不佩服果然是心思縝密啊
接口數(shù)量檢查
if (interfaces.length > 65535) {
throw new IllegalArgumentException("interface limit exceeded");
}
這邊限制了一個接口的數(shù)量為65535,那么這個65535是怎么來的呢
如果大家對Class
文件結(jié)構(gòu)有一定了解的話就知道文件中會通過interfaces_count
定義接口數(shù)量外冀,該數(shù)據(jù)項占2個字節(jié)襟沮,所以最大能表示65535
查找緩存
// If the proxy class defined by the given loader implementing
// the given interfaces exists, this will simply return the cached copy;
// otherwise, it will create the proxy class via the ProxyClassFactory
return proxyClassCache.get(loader, interfaces);
然后會在緩存中查找,這是肯定的膀跌,不然每次都重新生成一遍也太蠢了
這個緩存用的是WeakCache
對象捅伤,通過兩個key
來定位一個value
,就是通過我們傳入的類加載器和接口數(shù)組來定位一個動態(tài)生成的代理類祠汇,類似于Map<ClassLoader, Map<Class<?>[], Class<? entends Proxy>>>
// the key type is Object for supporting null key
private final ConcurrentMap<Object, ConcurrentMap<Object, Supplier<V>>> map
= new ConcurrentHashMap<>();
而他使用的key
和value
都是WeakReference
類型可很,防止當動態(tài)生成的類不再使用時導(dǎo)致內(nèi)存泄漏的問題
第一個key
凰浮,使用我們傳入的ClassLoader
為key
//key為我們傳入的ClassLoader
Object cacheKey = CacheKey.valueOf(key, refQueue);
private static final class CacheKey<K> extends WeakReference<K> {
//代碼省略
}
其中refQueue
是ReferenceQueue
類型(不了解的可以看一下WeakReference
的構(gòu)造器)而且都是同一個對象,ReferenceQueue
是屬于gc
回收這部分的內(nèi)容了菜拓,先不展開了吧
猜想由于ClassLoader
可能是URLClassLoader
甚至一些自定義的類加載器纳鼎,就有可能導(dǎo)致內(nèi)存泄漏裳凸,所以也用了弱引用
另外需要注意這個key
也就是類加載器是可以為null
的,猜測為null
時被判定為bootstrap
類加載器
但是如果我們傳入的類加載器為null
就可能會報錯xxx is not visible from class loader
贰逾,這是為什么呢
/*
* Verify that the class loader resolves the name of this
* interface to the same Class object.
*/
Class<?> interfaceClass = null;
try {
interfaceClass = Class.forName(intf.getName(), false, loader);
} catch (ClassNotFoundException e) {
}
if (interfaceClass != intf) {
throw new IllegalArgumentException(
intf + " is not visible from class loader");
}
可以看到通過用我們傳入的類加載器重新加載接口并判斷和我們傳入的接口是否相等來做校驗
我們都知道兩個Class
是否相等是需要滿足全限定名相等和類加載器相等這兩個條件的,而如果我們傳入null
也就是使用bootstrap
類加載器氯迂,那么和接口的類加載器AppClassLoader
是兩個不同的類加載器嚼蚀,導(dǎo)致兩個類不相等拋出異常
簡單來說,就是直接使用接口的類加載器就完事兒了
那么這個類加載器到底應(yīng)該怎么傳呢
如果是我們自定義的接口弄捕,那么需要傳入AppClassLoader
(或者是加載該接口的類加載器,當然也可以是以對應(yīng)類加載器為父類加載器的自定義類加載器)而不能傳入null
如果我們要代理的接口是java.util.List
這種穿铆,那就可以傳null
(本身就是由bootstrap
類加載器加載)或者AppClassLoader
(基于雙親委派模型能夠加載java.util.List
)
這里就不展開類加載器了(強制拉回)
繼續(xù)講第二個key
荞雏,第二個key
使用ClassLoader
和接口數(shù)組生成
// create subKey and retrieve the possible Supplier<V> stored by that
// subKey from valuesMap
Object subKey = Objects.requireNonNull(subKeyFactory.apply(key, parameter));
這里通過subKeyFactory
來生成一個subKey
平酿,subKeyFactory
是KeyFactory
對象
private static final WeakCache<ClassLoader, Class<?>[], Class<?>>
proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory());
不過KeyFactory
完全沒用到ClassLoader
,所以其實就是接口數(shù)組作為key
動態(tài)生成代理類
如果緩存中不存在筑辨,那么就需要通過ProxyClassFactory
來生成一個代理類
value = Objects.requireNonNull(valueFactory.apply(key, parameter));
valueFactory
也就是ProxyClassFactory
對象挖垛,我們來看看ProxyClassFactory
是怎么生成代理類的
接口校驗
首先會對我們傳入的接口進行ClassLoader
的校驗
/*
* Verify that the class loader resolves the name of this
* interface to the same Class object.
*/
Class<?> interfaceClass = null;
try {
interfaceClass = Class.forName(intf.getName(), false, loader);
} catch (ClassNotFoundException e) {
}
if (interfaceClass != intf) {
throw new IllegalArgumentException(
intf + " is not visible from class loader");
}
這塊前面已經(jīng)大致分析過了秉颗,即我們傳入的ClassLoader
需要是這些接口的類加載器
然后判斷這些接口(由于接口也是通過Class
表示所以需要額外校驗)必須為接口
/*
* Verify that the Class object actually represents an
* interface.
*/
if (!interfaceClass.isInterface()) {
throw new IllegalArgumentException(
interfaceClass.getName() + " is not an interface");
}
最后這些接口不能重復(fù)
/*
* Verify that this interface is not a duplicate.
*/
if (interfaceSet.put(interfaceClass, Boolean.TRUE) != null) {
throw new IllegalArgumentException(
"repeated interface: " + interfaceClass.getName());
}
設(shè)置訪問標志
int accessFlags = Modifier.PUBLIC | Modifier.FINAL;
首先會將代理類的訪問標志設(shè)置為public
和final
然后會判斷需要代理的接口蚕甥,如果存在不是public
的接口,則把代理類的訪問標志改為final
凭舶,并且將代理類的包名設(shè)置為非public
接口的包名爱沟,如果有多個非public
接口呼伸,就需要判斷這些非public
的接口包名是否一樣,否則拋出異常(因為如果不是public
就無法被其他包訪問到)
/*
* Record the package of a non-public proxy interface so that the
* proxy class will be defined in the same package. Verify that
* all non-public proxy interfaces are in the same package.
*/
for (Class<?> intf : interfaces) {
int flags = intf.getModifiers();
if (!Modifier.isPublic(flags)) {
accessFlags = Modifier.FINAL;
String name = intf.getName();
int n = name.lastIndexOf('.');
String pkg = ((n == -1) ? "" : name.substring(0, n + 1));
if (proxyPkg == null) {
proxyPkg = pkg;
} else if (!pkg.equals(proxyPkg)) {
throw new IllegalArgumentException(
"non-public interfaces from different packages");
}
}
}
確定最終包名
if (proxyPkg == null) {
// if no non-public proxy interfaces, use com.sun.proxy package
proxyPkg = ReflectUtil.PROXY_PACKAGE + ".";
}
如果上一步?jīng)]有非public
包名時搂根,那么指定包名為com.sun.proxy
剩愧,否則使用那個非public
接口的包名
指定類名
/*
* Choose a name for the proxy class to generate.
*/
long num = nextUniqueNumber.getAndIncrement();
String proxyName = proxyPkg + proxyClassNamePrefix + num;
其中proxyClassNamePrefix
為常量$Proxy
// prefix for all proxy class names
private static final String proxyClassNamePrefix = "$Proxy";
所以最后生成的代理類的全名就是com.sun.proxy.$Proxy0
娇斩,com.sun.proxy.$Proxy1
,com.sun.proxy.$Proxy2
以此類推(所有接口都是public
的情況下)
生成類文件
/*
* Generate the specified proxy class.
*/
byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
proxyName, interfaces, accessFlags);
public static byte[] generateProxyClass(final String name,
Class<?>[] interfaces,
int accessFlags) {
ProxyGenerator gen = new ProxyGenerator(name, interfaces, accessFlags);
final byte[] classFile = gen.generateClassFile();
//省略部分代碼
}
將代理類的全限定名藏否,需要實現(xiàn)的接口充包,訪問標志交給ProxyGenerator
去生成類文件
接下來看看generateClassFile
方法做了什么
添加Object方法
/*
* Record that proxy methods are needed for the hashCode, equals,
* and toString methods of java.lang.Object. This is done before
* the methods from the proxy interfaces so that the methods from
* java.lang.Object take precedence over duplicate methods in the
* proxy interfaces.
*/
addProxyMethod(hashCodeMethod, Object.class);
addProxyMethod(equalsMethod, Object.class);
addProxyMethod(toStringMethod, Object.class);
首先會添加hashCode
基矮,equals
和toString
這3個Object
的方法
添加接口方法
/*
* Now record all of the methods from the proxy interfaces, giving
* earlier interfaces precedence over later ones with duplicate
* methods.
*/
for (Class<?> intf : interfaces) {
for (Method m : intf.getMethods()) {
addProxyMethod(m, intf);
}
}
接著將需要被代理的所有接口方法也添加進去
方法返回值校驗
/*
* For each set of proxy methods with the same signature,
* verify that the methods' return types are compatible.
*/
for (List<ProxyMethod> sigmethods : proxyMethods.values()) {
checkReturnTypes(sigmethods);
}
之前我們添加的所有方法都會保存在proxyMethods
中,然后會校驗所有同名同入?yún)⒎椒ǖ姆祷刂?/p>
由于java
中不允許方法名相同本砰,入?yún)⑾嗤欠祷刂挡煌姆椒ǘx钢悲,比如
public interface Demo {
String demo(String s);
int demo(String s);
}
而我們平時寫代碼時莺琳,上面的寫法就直接報紅了
添加構(gòu)造方法
methods.add(generateConstructor());
接著添加構(gòu)造方法,generateConstructor
里面的內(nèi)容已經(jīng)是用byte[]
來拼Class
文件的內(nèi)容了珍手,就不跟進去講Class
文件結(jié)構(gòu)的內(nèi)容了
添加靜態(tài)方法變量
for (List<ProxyMethod> sigmethods : proxyMethods.values()) {
for (ProxyMethod pm : sigmethods) {
// add static field for method's Method object
fields.add(new FieldInfo(pm.methodFieldName,
"Ljava/lang/reflect/Method;",
ACC_PRIVATE | ACC_STATIC));
// generate code for proxy method and add it
methods.add(pm.generateMethod());
}
}
這里其實光看代碼會有點困惑辞做,不過如果看過最終生成的代理類就會比較好理解
這邊會把所有的方法都添加為代理類的private
和static
屬性字段秤茅,大家還記得InvocationHandler
的invoke
方法傳回來的其中一個參數(shù)就是Method
對象,實際上這個Method
對象就是這里添加的字段
而最后的generateMethod
方法也是拼接Class
文件內(nèi)容
添加靜態(tài)代碼塊的初始化方法
methods.add(generateStaticInitializer());
這里主要就是初始化上面定義的Method
類型的字段课幕,因為我們上面只是為每個方法都添加了一個Method
類型的字段定義帖努,但是這個字段并沒有賦值拼余,就是在靜態(tài)代碼塊中對這些字段進行了賦值
校驗字段和方法的數(shù)量
if (methods.size() > 65535) {
throw new IllegalArgumentException("method limit exceeded");
}
if (fields.size() > 65535) {
throw new IllegalArgumentException("field limit exceeded");
}
和接口數(shù)量一樣亩歹,方法數(shù)量和字段數(shù)量在Class
文件中也是用2個字節(jié)表示的凡橱,所以都有65535的限制
拼接類文件
/* ============================================================
* Step 3: Write the final class file.
*/
/*
* Make sure that constant pool indexes are reserved for the
* following items before starting to write the final class file.
*/
cp.getClass(dotToSlash(className));
cp.getClass(superclassName);
for (Class<?> intf: interfaces) {
cp.getClass(dotToSlash(intf.getName()));
}
/*
* Disallow new constant pool additions beyond this point, since
* we are about to write the final constant pool table.
*/
cp.setReadOnly();
ByteArrayOutputStream bout = new ByteArrayOutputStream();
DataOutputStream dout = new DataOutputStream(bout);
try {
/*
* Write all the items of the "ClassFile" structure.
* See JVMS section 4.1.
*/
// u4 magic;
dout.writeInt(0xCAFEBABE);
// u2 minor_version;
dout.writeShort(CLASSFILE_MINOR_VERSION);
// u2 major_version;
dout.writeShort(CLASSFILE_MAJOR_VERSION);
cp.write(dout); // (write constant pool)
// u2 access_flags;
dout.writeShort(accessFlags);
// u2 this_class;
dout.writeShort(cp.getClass(dotToSlash(className)));
// u2 super_class;
dout.writeShort(cp.getClass(superclassName));
// u2 interfaces_count;
dout.writeShort(interfaces.length);
// u2 interfaces[interfaces_count];
for (Class<?> intf : interfaces) {
dout.writeShort(cp.getClass(
dotToSlash(intf.getName())));
}
// u2 fields_count;
dout.writeShort(fields.size());
// field_info fields[fields_count];
for (FieldInfo f : fields) {
f.write(dout);
}
// u2 methods_count;
dout.writeShort(methods.size());
// method_info methods[methods_count];
for (MethodInfo m : methods) {
m.write(dout);
}
// u2 attributes_count;
dout.writeShort(0); // (no ClassFile attributes for proxy classes)
} catch (IOException e) {
throw new InternalError("unexpected I/O Exception", e);
}
這里其實沒啥好說的,這是Class
文件結(jié)構(gòu)的內(nèi)容了坝撑,包括上面的generateConstructor
粮揉,generateMethod
,generateStaticInitializer
都是類似的代碼
重點講一下這句代碼
// u2 super_class;
dout.writeShort(cp.getClass(superclassName));
這里的superclassName
為常量java/lang/reflect/Proxy
/** name of the superclass of proxy classes */
private final static String superclassName = "java/lang/reflect/Proxy";
所以所有的代理類的父類都是Proxy
這個類侨拦,這也就是jdk
動態(tài)代理只能代理接口的原因(java
不允許多繼承)
定義類
try {
return defineClass0(loader, proxyName,
proxyClassFile, 0, proxyClassFile.length);
} catch (ClassFormatError e) {
/*
* A ClassFormatError here means that (barring bugs in the
* proxy class generation code) there was some other
* invalid aspect of the arguments supplied to the proxy
* class creation (such as virtual machine limitations
* exceeded).
*/
throw new IllegalArgumentException(e.toString());
}
這個方法是native
的狱从,就不深入了
總結(jié)
是不是突然覺得可以手擼一套定制化的動態(tài)代理框架了(如果對Class
文件比較熟悉叠纹,或者借助ASM
)
其實jdk
動態(tài)代理的整個邏輯并沒有多復(fù)雜,無非就是按照Class
文件的結(jié)構(gòu)要求拼接各個部分數(shù)據(jù)训貌,但是在整個過程中做了很多校驗的邏輯
相對應(yīng)我們平時的開發(fā)冒窍,業(yè)務(wù)功能其實很多情況下都不復(fù)雜甚至還非常簡單综液,但是業(yè)務(wù)定義之外的邊緣數(shù)據(jù)的校驗和適配也不能馬虎,又或是對于一些并發(fā)等其他場景下的風險考慮檩奠,邏輯嚴密性比功能實現(xiàn)來的更為重要