本文主要介紹Java
中兩種常見的動態代理方式:JDK原生動態代理
和CGLIB動態代理
。
什么是代理模式
就是為其他對象提供一種代理以控制對這個對象的訪問。代理可以在不改動目標對象的基礎上,增加其他額外的功能(擴展功能)。
代理模式角色分為 3 種:
Subject
(抽象主題角色):定義代理類和真實主題的公共對外方法,也是代理類代理真實主題的方法;RealSubject
(真實主題角色):真正實現業務邏輯的類;Proxy
(代理主題角色):用來代理和封裝真實主題;
如果根據字節碼的創建時機來分類,可以分為靜態代理和動態代理:
- 所謂靜態也就是在程序運行前就已經存在代理類的字節碼文件,代理類和真實主題角色的關系在運行前就確定了。
- 而動態代理的源碼是在程序運行期間由JVM根據反射等機制動態的生成,所以在運行前並不存在代理類的字節碼文件
靜態代理
學習動態代理前,有必要來學習一下靜態代理。
靜態代理在使用時,需要定義接口或者父類,被代理對象(目標對象)與代理對象(Proxy)一起實現相同的接口或者是繼承相同父類。
來看一個例子,模擬小貓走路的時間。
// 接口
public interface Walkable {
void walk();
}
// 實現類
public class Cat implements Walkable {
@Override
public void walk() {
System.out.println("cat is walking...");
try {
Thread.sleep(new Random().nextInt(1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
如果我想知道走路的時間怎么辦?可以將實現類Cat
修改為:
public class Cat implements Walkable {
@Override
public void walk() {
long start = System.currentTimeMillis();
System.out.println("cat is walking...");
try {
Thread.sleep(new Random().nextInt(1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println("walk time = " + (end - start));
}
}
這里已經侵入了源代碼,如果源代碼是不能改動的,這樣寫顯然是不行的,這里可以引入時間代理類CatTimeProxy
。
public class CatTimeProxy implements Walkable {
private Walkable walkable;
public CatTimeProxy(Walkable walkable) {
this.walkable = walkable;
}
@Override
public void walk() {
long start = System.currentTimeMillis();
walkable.walk();
long end = System.currentTimeMillis();
System.out.println("Walk time = " + (end - start));
}
}
如果這時候還要加上常見的日志功能,我們還需要創建一個日志代理類CatLogProxy
。
public class CatLogProxy implements Walkable {
private Walkable walkable;
public CatLogProxy(Walkable walkable) {
this.walkable = walkable;
}
@Override
public void walk() {
System.out.println("Cat walk start...");
walkable.walk();
System.out.println("Cat walk end...");
}
}
如果我們需要先記錄日志,再獲取行走時間,可以在調用的地方這么做:
public static void main(String[] args) {
Cat cat = new Cat();
CatLogProxy p1 = new CatLogProxy(cat);
CatTimeProxy p2 = new CatTimeProxy(p1);
p2.walk();
}
這樣的話,計時是包括打日志的時間的。
靜態代理的問題
如果我們需要計算SDK
中100個方法的運行時間,同樣的代碼至少需要重復100次,並且創建至少100個代理類。往小了說,如果Cat
類有多個方法,我們需要知道其他方法的運行時間,同樣的代碼也至少需要重復多次。因此,靜態代理至少有以下兩個局限性問題:
- 如果同時代理多個類,依然會導致類無限制擴展
- 如果類中有多個方法,同樣的邏輯需要反復實現
所以,我們需要一個通用的代理類來代理所有的類的所有方法,這就需要用到動態代理技術。
動態代理
學習任何一門技術,一定要問一問自己,這到底有什么用。其實,在這篇文章的講解過程中,我們已經說出了它的主要用途。你發現沒,使用動態代理我們居然可以在不改變源碼的情況下,直接在方法中插入自定義邏輯。這有點不太符合我們的一條線走到底的編程邏輯,這種編程模型有一個專業名稱叫AOP
。所謂的AOP
,就像刀一樣,抓住時機,趁機插入。
Jdk動態代理
JDK實現代理只需要使用newProxyInstance方法,但是該方法需要接收三個參數:
@CallerSensitive
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
throws IllegalArgumentException
{
Objects.requireNonNull(h);
final Class<?>[] intfs = interfaces.clone();
final SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
}
/*
* Look up or generate the designated proxy class.
*/
Class<?> cl = getProxyClass0(loader, intfs);
/*
* Invoke its constructor with the designated invocation handler.
*/
try {
if (sm != null) {
checkNewProxyPermission(Reflection.getCallerClass(), cl);
}
final Constructor<?> cons = cl.getConstructor(constructorParams);
final InvocationHandler ih = h;
if (!Modifier.isPublic(cl.getModifiers())) {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
cons.setAccessible(true);
return null;
}
});
}
return cons.newInstance(new Object[]{h});
} catch (IllegalAccessException|InstantiationException e) {
throw new InternalError(e.toString(), e);
} catch (InvocationTargetException e) {
Throwable t = e.getCause();
if (t instanceof RuntimeException) {
throw (RuntimeException) t;
} else {
throw new InternalError(t.toString(), t);
}
} catch (NoSuchMethodException e) {
throw new InternalError(e.toString(), e);
}
}
方法是在Proxy
類中是靜態方法,且接收的三個參數依次為:
ClassLoader loader
//指定當前目標對象使用類加載器Class<?>[] interfaces
//目標對象實現的接口的類型,使用泛型方式確認類型InvocationHandler h
//事件處理器
主要是完成InvocationHandler h
的編寫工作。
接口類UserService
:
public interface UserService {
public void select();
public void update();
}
接口實現類,即要代理的類UserServiceImpl
:
public class UserServiceImpl implements UserService {
@Override
public void select() {
System.out.println("查詢 selectById");
}
@Override
public void update() {
System.out.println("更新 update");
}
}
代理類UserServiceProxy
:
public class UserServiceProxy implements UserService {
private UserService target;
public UserServiceProxy(UserService target){
this.target = target;
}
@Override
public void select() {
before();
target.select();
after();
}
@Override
public void update() {
before();
target.update();
after();
}
private void before() { // 在執行方法之前執行
System.out.println(String.format("log start time [%s] ", new Date()));
}
private void after() { // 在執行方法之后執行
System.out.println(String.format("log end time [%s] ", new Date()));
}
}
主程序類:
public class UserServiceProxyJDKMain {
public static void main(String[] args) {
// 1. 創建被代理的對象,即UserService的實現類
UserServiceImpl userServiceImpl = new UserServiceImpl();
// 2. 獲取對應的classLoader
ClassLoader classLoader = userServiceImpl.getClass().getClassLoader();
// 3. 獲取所有接口的Class, 這里的userServiceImpl只實現了一個接口UserService,
Class[] interfaces = userServiceImpl.getClass().getInterfaces();
// 4. 創建一個將傳給代理類的調用請求處理器,處理所有的代理對象上的方法調用
// 這里創建的是一個自定義的日志處理器,須傳入實際的執行對象 userServiceImpl
InvocationHandler logHandler = new LogHandler(userServiceImpl);
/*
5.根據上面提供的信息,創建代理對象 在這個過程中,
a.JDK會通過根據傳入的參數信息動態地在內存中創建和.class 文件等同的字節碼
b.然后根據相應的字節碼轉換成對應的class,
c.然后調用newInstance()創建代理實例
*/
// 會動態生成UserServiceProxy代理類,並且用代理對象實例化LogHandler,調用代理對象的.invoke()方法即可
UserService proxy = (UserService) Proxy.newProxyInstance(classLoader, interfaces, logHandler);
// 調用代理的方法
proxy.select();
proxy.update();
// 生成class文件的名稱
ProxyUtils.generateClassFile(userServiceImpl.getClass(), "UserServiceJDKProxy");
}
}
這里可以保存下來代理生成的實現了接口的代理對象:
public class ProxyUtils {
/*
* 將根據類信息 動態生成的二進制字節碼保存到硬盤中,
* 默認的是clazz目錄下
* params :clazz 需要生成動態代理類的類
* proxyName : 為動態生成的代理類的名稱
*/
public static void generateClassFile(Class clazz, String proxyName) {
//根據類信息和提供的代理類名稱,生成字節碼
byte[] classFile = ProxyGenerator.generateProxyClass(proxyName, clazz.getInterfaces());
String paths = clazz.getResource(".").getPath();
System.out.println(paths);
FileOutputStream out = null;
try {
//保留到硬盤中
out = new FileOutputStream(paths + proxyName + ".class");
out.write(classFile);
out.flush();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
動態代理實現過程
- 通過
getProxyClass0()
生成代理類。JDK
生成的最終真正的代理類,它繼承自Proxy
並實現了我們定義的接口. - 通過
Proxy.newProxyInstance()
生成代理類的實例對象,創建對象時傳入InvocationHandler
類型的實例。 - 調用新實例的方法,即原
InvocationHandler
類中的invoke()
方法。
代理對象不需要實現接口,但是目標對象一定要實現接口,否則不能用動態代理
Cglib動態代理
JDK
的動態代理機制只能代理實現了接口的類,而不能實現接口的類就不能實現JDK
的動態代理,cglib
是針對類來實現代理的,他的原理是對指定的目標類生成一個子類,並覆蓋其中方法實現增強,但因為采用的是繼承,所以不能對final
修飾的類進行代理。
Cglib
代理,也叫作子類代理,它是在內存中構建一個子類對象從而實現對目標對象功能的擴展。
Cglib
子類代理實現方法:
- 需要引入
cglib
的jar
文件,但是Spring
的核心包中已經包括了Cglib
功能,所以直接引入Spring-core.jar
即可. - 引入功能包后,就可以在內存中動態構建子類
- 代理的類不能為
final
,否則報錯 - 目標對象的方法如果為
final/static
,那么就不會被攔截,即不會執行目標對象額外的業務方法.
基本使用
<!-- https://mvnrepository.com/artifact/cglib/cglib -->
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>2.2</version>
</dependency>
方法攔截器
public class LogInterceptor implements MethodInterceptor{
/*
* @param o 要進行增強的對象
* @param method 要攔截的方法
* @param objects 參數列表,基本數據類型需要傳入其包裝類
* @param methodProxy 對方法的代理,
* @return 執行結果
* @throws Throwable
*/
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
before();
Object result = methodProxy.invokeSuper(o, objects);
after();
return result;
}
private void before() {
System.out.println(String.format("log start time [%s] ", new Date()));
}
private void after() {
System.out.println(String.format("log end time [%s] ", new Date()));
}
}
測試用例
這里保存了代理類的.class
文件
public class CglibMain {
public static void main(String[] args) {
// 創建Enhancer對象,類似於JDK動態代理的Proxy類
Enhancer enhancer = new Enhancer();
// 設置目標類的字節碼文件
enhancer.setSuperclass(UserDao.class);
// 設置回調函數
enhancer.setCallback(new LogInterceptor());
// create會創建代理類
UserDao userDao = (UserDao)enhancer.create();
userDao.update();
userDao.select();
}
}
結果
log start time [Mon Nov 30 17:26:39 CST 2020]
UserDao 更新 update
log end time [Mon Nov 30 17:26:39 CST 2020]
log start time [Mon Nov 30 17:26:39 CST 2020]
UserDao 查詢 selectById
log end time [Mon Nov 30 17:26:39 CST 2020]
JDK動態代理與CGLIB動態代理對比
JDK 動態代理
- 為了解決靜態代理中,生成大量的代理類造成的冗余;
JDK
動態代理只需要實現InvocationHandler
接口,重寫invoke
方法便可以完成代理的實現,- jdk的代理是利用反射生成代理類
Proxyxx.class
代理類字節碼,並生成對象 - jdk動態代理之所以只能代理接口是因為代理類本身已經
extends
了Proxy
,而java是不允許多重繼承的,但是允許實現多個接口
優點:解決了靜態代理中冗余的代理實現類問題。
缺點:JDK
動態代理是基於接口設計實現的,如果沒有接口,會拋異常。
CGLIB 代理
- 由於
JDK
動態代理限制了只能基於接口設計,而對於沒有接口的情況,JDK
方式解決不了; CGLib
采用了非常底層的字節碼技術,其原理是通過字節碼技術為一個類創建子類,並在子類中采用方法攔截的技術攔截所有父類方法的調用,順勢織入橫切邏輯,來完成動態代理的實現。- 實現方式實現
MethodInterceptor
接口,重寫intercept
方法,通過Enhancer
類的回調方法來實現。 - 但是
CGLib
在創建代理對象時所花費的時間卻比JDK多得多,所以對於單例的對象,因為無需頻繁創建對象,用CGLib
合適,反之,使用JDK
方式要更為合適一些。 - 同時,由於
CGLib
由於是采用動態創建子類的方法,對於final
方法,無法進行代理。
優點:沒有接口也能實現動態代理,而且采用字節碼增強技術,性能也不錯。
缺點:技術實現相對難理解些。
Hive系列文章
Hive表的基本操作
Hive中的集合數據類型
Hive動態分區詳解
hive中orc格式表的數據導入
Java通過jdbc連接hive
通過HiveServer2訪問Hive
SpringBoot連接Hive實現自助取數
hive關聯hbase表
Hive udf 使用方法
Hive基於UDF進行文本分詞
Hive窗口函數row number的用法
數據倉庫之拉鏈表