動態代理學習記錄
JDK
動態代理
使用
- 首先定義一個介面類
// Person.java
package com.may.learning;
public interface Person {
void check();
void checkout();
}
- 再定義實現該介面的類,用以生成需要被代理的例項物件
// Employee.java package com.may.learning; public class Employee implements Person{ private String name; public Employee(String name) { this.name = name; } public void check() { System.out.printf("%s 今天也要好好上班!\n", name); } public void checkout() { System.out.printf("%s 明天見\n", name); } }
- 使用
InvocationHandler
定義invoke
方法,用以替代被代理物件執行時的方法,並且使用newProxyInstance建立一個代理類.
// Welcome.java package com.may.learning; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; public class Welcome { static class LogHandler implements InvocationHandler { Object obj; public LogHandler(Object obj) { this.obj = obj; } public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("準備打卡"); method.invoke(obj, args); System.out.println("打卡完成"); return null; } } public static void main(String[] args) { Person p = new Employee("littlemay"); LogHandler logHandler = new LogHandler(p); p = (Person) Proxy.newProxyInstance(p.getClass().getClassLoader(), p.getClass().getInterfaces(), logHandler); p.check(); p.checkout(); } }
執行結果:
準備打卡
littlemay 今天也要好好上班!
打卡完成
準備打卡
littlemay 明天見
打卡完成
說明
- 進行呼叫的物件
p
事實上是根據Person
介面的位元組碼臨時構造的一個物件.通過Proxy.newProxyInstance
方法進行構造,接收三個引數:
loader
: 代理類的ClassLoader
interfaces
:代理類需要實現的介面handler
:呼叫處理器例項,在這裡是logHandler
InvocationHandler
的invoke
方法接收三個引數:
proxy
: 代理後的物件,在這裡是pmethod
: 被代理物件需要代理的方法args
在實現InvocationHandler
中,傳入的proxy
物件,和方法呼叫時的obj
,需要區分開,並不是一個東西:
雖然proxy
看似無用,但是卻是可以實現鏈式呼叫進行返回的.參照:Understanding “proxy” arguments of the invoke method of java.lang.reflect.InvocationHandler.
而obj
是需要傳入的被代理物件例項.
比如在此基礎上做一個邊上班邊增加工資的無聊功能:
// Person.java
package com.may.learning;
public interface Person {
void check();
void checkout();
// new
Person addSalary(double money);
double getSalary();
}
// Employee.java
package com.may.learning;
public class Employee implements Person{
private String name;
private Double money;
public Employee(String name, Double money) {
this.name = name;
this.money = money;
}
public void check() {
System.out.printf("%s 今天也要好好上班!\n", name);
}
public void checkout() {
System.out.printf("%s 明天見\n", name);
}
// new
public Person addSalary(double money){
this.money += money;
return this;
}
public double getSalary() {
return money;
}
}
// Welcome.java
package com.may.learning;
import java.io.IOException;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class Welcome {
public static void main(String[] args) throws IOException {
Person p = new Employee("littlemay", 10000.0);
LogHandler logHandler = new LogHandler(p);
p = (Person) Proxy
.newProxyInstance(p.getClass().getClassLoader(), p.getClass().getInterfaces(),
logHandler);
p.check();
System.out.println("工資餘額: "+ p.getSalary());
// 鏈式呼叫
p.addSalary(100).addSalary(100).addSalary(1000);
p.checkout();
System.out.println("工資餘額: "+ p.getSalary());
}
static class LogHandler implements InvocationHandler {
Object obj;
public LogHandler(Object obj) {
this.obj = obj;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ("check".equals(method.getName())) {
System.out.println("準備打卡");
method.invoke(obj, args);
System.out.println("打卡成功");
} else if ("addSalary".equals(method.getName())) {
method.invoke(obj, args);
System.out.println("工資增加了");
} else{
return method.invoke(obj, args);
}
return proxy;
}
}
}
輸出結果:
準備打卡
littlemay 今天也要好好上班!
打卡成功
工資餘額: 10000.0
工資增加了
工資增加了
工資增加了
littlemay 明天見
工資餘額: 11200.0
為什麼說JDK
代理只能基於介面實現呢
newProxyInstance
是生成代理物件的class
檔案,更改程式碼獲得生成的class
檔案:
public static void main(String[] args) throws IOException {
Person p = new Employee("littlemay");
LogHandler logHandler = new LogHandler(p);
p = (Person) Proxy.newProxyInstance(p.getClass().getClassLoader(), p.getClass().getInterfaces(), logHandler);
// 獲取位元組碼
byte[] proxyClassFile = ProxyGenerator.generateProxyClass("com.may.learning.$Proxy0", p.getClass().getInterfaces(),
Modifier.PUBLIC);
File file = new File("$Proxy0.class");
OutputStream outputStream = new FileOutputStream(file);
outputStream.write(proxyClassFile);
// end
p.check();
p.checkout();
}
檢視位元組碼:
// $Proxy0.class
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package com.may.learning;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;
public class $Proxy0 extends Proxy implements Person {
private static Method m1;
private static Method m2;
private static Method m4;
private static Method m3;
private static Method m0;
public $Proxy0(InvocationHandler var1) throws {
super(var1);
}
public final boolean equals(Object var1) throws {
try {
return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
} catch (RuntimeException | Error var3) {
throw var3;
} catch (Throwable var4) {
throw new UndeclaredThrowableException(var4);
}
}
public final String toString() throws {
try {
return (String)super.h.invoke(this, m2, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final void check() throws {
try {
super.h.invoke(this, m4, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final void checkout() throws {
try {
super.h.invoke(this, m3, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final int hashCode() throws {
try {
return (Integer)super.h.invoke(this, m0, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
static {
try {
m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
m2 = Class.forName("java.lang.Object").getMethod("toString");
m4 = Class.forName("com.may.learning.Person").getMethod("check");
m3 = Class.forName("com.may.learning.Person").getMethod("checkout");
m0 = Class.forName("java.lang.Object").getMethod("hashCode");
} catch (NoSuchMethodException var2) {
throw new NoSuchMethodError(var2.getMessage());
} catch (ClassNotFoundException var3) {
throw new NoClassDefFoundError(var3.getMessage());
}
}
}
可以發現$Proxy0
已經繼承了Proxy
類,因為java
不支援多繼承,所以只能採用實現Person
介面的方式來實現.
參考資料:
你真的完全瞭解Java動態代理嗎?看這篇就夠了
Java動態代理
為什麼JDK的動態代理要基於介面實現而不能基於繼承實現?
Java中InvocationHandler介面中第一個引數proxy詳解
cglib動態代理
由於JDK
動態代理只能基於介面實現,如果要對類進行代理,那麼可以使用cglib
動態代理.它是一個基於ASM
的位元組碼生成庫,允許在執行時對位元組碼進行修改和動態生成。
使用
- 新增依賴
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>2.2.2</version>
</dependency>
- 編寫一個類
// UserDao.java
package com.may.learning;
public class UserDao {
public void update() {
System.out.println("update...");
}
public void select() {
System.out.println("select...");
}
public final void show(){
System.out.println("show");
}
}
- 編寫代理,
intercept
有四個引數,obj
表示代理物件,method
表示目標類中的方法,args
表示方法引數,proxy
表示代理方法的MethodProxy
物件
// UserDaoInterceptor.java
package com.may.learning;
import java.lang.reflect.Method;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
public class UserDaoInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
// System.out.println(obj.getClass().getName());
System.out.println("transaction open");
Object o = proxy.invokeSuper(obj, args);
System.out.println("transaction close");
return o;
}
}
需要呼叫invokeSuper
方法,如果呼叫invoke
會死迴圈導致棧溢位.原因在於:
invoke
最終執行的是fci.f1.invoke(fci.i1, obj, args)
;invokeSuper
最終執行的是fci.f2.invoke(fci.i2, obj, args)
至於f1
,f2
分別代表什麼,需要檢視FastClassInfo
.(FastClass
機制就是對一個類的方法建立索引,通過索引來直接呼叫相應的方法.)
private static class FastClassInfo {
FastClass f1; // 被代理物件的fastclass
FastClass f2; // 代理後物件的fastclass
int i1; // 被代理方法在f1中的索引
int i2; // 代理後方法在f2中的索引
}
因此如果使用invoke
,最終還是呼叫被代理物件的被代理方法,仍然會進入到intercept
中,如此迴圈下去.
- 利用
enhancer.create()
獲取代理物件,進行呼叫
package com.may.learning;
import org.springframework.cglib.core.DebuggingClassWriter;
import org.springframework.cglib.proxy.Enhancer;
public class TestCglib {
public static void main(String[] args) {
System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY,"/home/may/learning/java");
Enhancer enhancer = new Enhancer();
// 設定要代理的類
enhancer.setSuperclass(UserDao.class);
//設定回撥
enhancer.setCallback(new UserDaoInterceptor());
// 生成代理物件
UserDao userDao = (UserDao)enhancer.create();
userDao.select();
userDao.update();
userDao.show();
}
}
可以利用
System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY,"/home/may/learning/java");
檢視生成的位元組碼
說明
所有非final
的方法都會轉發到UserDaoInterceptor.invoke()
方法中.所以show
方法不會有事務開啟關閉的語句輸出.呼叫結果為:
transaction open
select...
transaction close
transaction open
update...
transaction close
show
因為每一次列印都會呼叫
toString
方法,所以如果在invoke
方法中執行列印obj
,會導致stackoverflow
,這在兩種動態代理方法都會出現.並且打斷點除錯會輸出更多的語句.
檢視生成的UserDao$$EnhancerByCGLIB$$8a7c533a.class
檔案,發現如果存在攔截器,會使用攔截器執行:
public final void update() {
MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
if (var10000 == null) {
CGLIB$BIND_CALLBACKS(this);
var10000 = this.CGLIB$CALLBACK_0;
}
if (var10000 != null) {
var10000.intercept(this, CGLIB$update$1$Method, CGLIB$emptyArgs, CGLIB$update$1$Proxy);
} else {
super.update();
}
}
參考資料:
Java Proxy和CGLIB動態代理原理
cglib原始碼分析(四):cglib 動態代理原理分析
CGLib動態代理