Java動(dòng)態(tài)代理
本文主要介紹Java
中兩種常見的動(dòng)態(tài)代理方式:JDK原生動(dòng)態(tài)代理
和CGLIB動(dòng)態(tài)代理
。
什么是代理模式
就是為其他對(duì)象提供一種代理以控制對(duì)這個(gè)對(duì)象的訪問。代理可以在不改動(dòng)目標(biāo)對(duì)象的基礎(chǔ)上,增加其他額外的功能(擴(kuò)展功能)。
代理模式角色分為 3 種:
Subject
(抽象主題角色):定義代理類和真實(shí)主題的公共對(duì)外方法,也是代理類代理真實(shí)主題的方法;RealSubject
(真實(shí)主題角色):真正實(shí)現(xiàn)業(yè)務(wù)邏輯的類;Proxy
(代理主題角色):用來代理和封裝真實(shí)主題;
如果根據(jù)字節(jié)碼的創(chuàng)建時(shí)機(jī)來分類,可以分為靜態(tài)代理和動(dòng)態(tài)代理:
- 所謂靜態(tài)也就是在程序運(yùn)行前就已經(jīng)存在代理類的字節(jié)碼文件,代理類和真實(shí)主題角色的關(guān)系在運(yùn)行前就確定了。
- 而動(dòng)態(tài)代理的源碼是在程序運(yùn)行期間由JVM根據(jù)反射等機(jī)制動(dòng)態(tài)的生成,所以在運(yùn)行前并不存在代理類的字節(jié)碼文件
靜態(tài)代理
學(xué)習(xí)動(dòng)態(tài)代理前,有必要來學(xué)習(xí)一下靜態(tài)代理。
靜態(tài)代理在使用時(shí),需要定義接口或者父類,被代理對(duì)象(目標(biāo)對(duì)象)與代理對(duì)象(Proxy)一起實(shí)現(xiàn)相同的接口或者是繼承相同父類。
來看一個(gè)例子,模擬小貓走路的時(shí)間。
- // 接口
- public interface Walkable {
- void walk();
- }
-
- // 實(shí)現(xiàn)類
- 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();
- }
- }
- }
Java
如果我想知道走路的時(shí)間怎么辦?可以將實(shí)現(xiàn)類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));
- }
- }
Java
這里已經(jīng)侵入了源代碼,如果源代碼是不能改動(dòng)的,這樣寫顯然是不行的,這里可以引入時(shí)間代理類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));
- }
- }
Java
如果這時(shí)候還要加上常見的日志功能,我們還需要?jiǎng)?chuàng)建一個(gè)日志代理類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...");
-
- }
- }
Java
如果我們需要先記錄日志,再獲取行走時(shí)間,可以在調(diào)用的地方這么做:
- public static void main(String[] args) {
- Cat cat = new Cat();
- CatLogProxy p1 = new CatLogProxy(cat);
- CatTimeProxy p2 = new CatTimeProxy(p1);
-
- p2.walk();
- }
Java
這樣的話,計(jì)時(shí)是包括打日志的時(shí)間的。
靜態(tài)代理的問題
如果我們需要計(jì)算SDK
中100個(gè)方法的運(yùn)行時(shí)間,同樣的代碼至少需要重復(fù)100次,并且創(chuàng)建至少100個(gè)代理類。往小了說,如果Cat
類有多個(gè)方法,我們需要知道其他方法的運(yùn)行時(shí)間,同樣的代碼也至少需要重復(fù)多次。因此,靜態(tài)代理至少有以下兩個(gè)局限性問題:
- 如果同時(shí)代理多個(gè)類,依然會(huì)導(dǎo)致類無限制擴(kuò)展
- 如果類中有多個(gè)方法,同樣的邏輯需要反復(fù)實(shí)現(xiàn)
所以,我們需要一個(gè)通用的代理類來代理所有的類的所有方法,這就需要用到動(dòng)態(tài)代理技術(shù)。
動(dòng)態(tài)代理
學(xué)習(xí)任何一門技術(shù),一定要問一問自己,這到底有什么用。其實(shí),在這篇文章的講解過程中,我們已經(jīng)說出了它的主要用途。你發(fā)現(xiàn)沒,使用動(dòng)態(tài)代理我們居然可以在不改變源碼的情況下,直接在方法中插入自定義邏輯。這有點(diǎn)不太符合我們的一條線走到底的編程邏輯,這種編程模型有一個(gè)專業(yè)名稱叫AOP
。所謂的AOP
,就像刀一樣,抓住時(shí)機(jī),趁機(jī)插入。
Jdk動(dòng)態(tài)代理
JDK實(shí)現(xiàn)代理只需要使用newProxyInstance方法,但是該方法需要接收三個(gè)參數(shù):
- @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);
- }
- }
Java
方法是在Proxy
類中是靜態(tài)方法,且接收的三個(gè)參數(shù)依次為:
ClassLoader loader
//指定當(dāng)前目標(biāo)對(duì)象使用類加載器Class<?>[] interfaces
//目標(biāo)對(duì)象實(shí)現(xiàn)的接口的類型,使用泛型方式確認(rèn)類型InvocationHandler h
//事件處理器
主要是完成InvocationHandler h
的編寫工作。
接口類UserService
:
- public interface UserService {
-
- public void select();
-
- public void update();
- }
Java
接口實(shí)現(xiàn)類,即要代理的類UserServiceImpl
:
- public class UserServiceImpl implements UserService {
- @Override
- public void select() {
- System.out.println("查詢 selectById");
- }
-
- @Override
- public void update() {
- System.out.println("更新 update");
- }
- }
Java
代理類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() { // 在執(zhí)行方法之前執(zhí)行
- System.out.println(String.format("log start time [%s] ", new Date()));
- }
- private void after() { // 在執(zhí)行方法之后執(zhí)行
- System.out.println(String.format("log end time [%s] ", new Date()));
- }
- }
Java
主程序類:
- public class UserServiceProxyJDKMain {
- public static void main(String[] args) {
-
- // 1. 創(chuàng)建被代理的對(duì)象,即UserService的實(shí)現(xiàn)類
- UserServiceImpl userServiceImpl = new UserServiceImpl();
-
- // 2. 獲取對(duì)應(yīng)的classLoader
- ClassLoader classLoader = userServiceImpl.getClass().getClassLoader();
-
- // 3. 獲取所有接口的Class, 這里的userServiceImpl只實(shí)現(xiàn)了一個(gè)接口UserService,
- Class[] interfaces = userServiceImpl.getClass().getInterfaces();
-
- // 4. 創(chuàng)建一個(gè)將傳給代理類的調(diào)用請求處理器,處理所有的代理對(duì)象上的方法調(diào)用
- // 這里創(chuàng)建的是一個(gè)自定義的日志處理器,須傳入實(shí)際的執(zhí)行對(duì)象 userServiceImpl
- InvocationHandler logHandler = new LogHandler(userServiceImpl);
-
- /*
- 5.根據(jù)上面提供的信息,創(chuàng)建代理對(duì)象 在這個(gè)過程中,
- a.JDK會(huì)通過根據(jù)傳入的參數(shù)信息動(dòng)態(tài)地在內(nèi)存中創(chuàng)建和.class 文件等同的字節(jié)碼
- b.然后根據(jù)相應(yīng)的字節(jié)碼轉(zhuǎn)換成對(duì)應(yīng)的class,
- c.然后調(diào)用newInstance()創(chuàng)建代理實(shí)例
- */
-
- // 會(huì)動(dòng)態(tài)生成UserServiceProxy代理類,并且用代理對(duì)象實(shí)例化LogHandler,調(diào)用代理對(duì)象的.invoke()方法即可
- UserService proxy = (UserService) Proxy.newProxyInstance(classLoader, interfaces, logHandler);
-
- // 調(diào)用代理的方法
- proxy.select();
- proxy.update();
-
- // 生成class文件的名稱
- ProxyUtils.generateClassFile(userServiceImpl.getClass(), "UserServiceJDKProxy");
- }
- }
Java
這里可以保存下來代理生成的實(shí)現(xiàn)了接口的代理對(duì)象:
- public class ProxyUtils {
- /*
- * 將根據(jù)類信息 動(dòng)態(tài)生成的二進(jìn)制字節(jié)碼保存到硬盤中,
- * 默認(rèn)的是clazz目錄下
- * params :clazz 需要生成動(dòng)態(tài)代理類的類
- * proxyName : 為動(dòng)態(tài)生成的代理類的名稱
- */
- public static void generateClassFile(Class clazz, String proxyName) {
-
- //根據(jù)類信息和提供的代理類名稱,生成字節(jié)碼
- 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();
- }
- }
- }
-
- }
Java
動(dòng)態(tài)代理實(shí)現(xiàn)過程
- 通過
getProxyClass0()
生成代理類。JDK
生成的最終真正的代理類,它繼承自Proxy
并實(shí)現(xiàn)了我們定義的接口. - 通過
Proxy.newProxyInstance()
生成代理類的實(shí)例對(duì)象,創(chuàng)建對(duì)象時(shí)傳入InvocationHandler
類型的實(shí)例。 - 調(diào)用新實(shí)例的方法,即原
InvocationHandler
類中的invoke()
方法。
代理對(duì)象不需要實(shí)現(xiàn)接口,但是目標(biāo)對(duì)象一定要實(shí)現(xiàn)接口,否則不能用動(dòng)態(tài)代理
Cglib動(dòng)態(tài)代理
JDK
的動(dòng)態(tài)代理機(jī)制只能代理實(shí)現(xiàn)了接口的類,而不能實(shí)現(xiàn)接口的類就不能實(shí)現(xiàn)JDK
的動(dòng)態(tài)代理,cglib
是針對(duì)類來實(shí)現(xiàn)代理的,他的原理是對(duì)指定的目標(biāo)類生成一個(gè)子類,并覆蓋其中方法實(shí)現(xiàn)增強(qiáng),但因?yàn)椴捎玫氖抢^承,所以不能對(duì)final
修飾的類進(jìn)行代理。
Cglib
代理,也叫作子類代理,它是在內(nèi)存中構(gòu)建一個(gè)子類對(duì)象從而實(shí)現(xiàn)對(duì)目標(biāo)對(duì)象功能的擴(kuò)展。
Cglib
子類代理實(shí)現(xiàn)方法:
- 需要引入
cglib
的jar
文件,但是Spring
的核心包中已經(jīng)包括了Cglib
功能,所以直接引入Spring-core.jar
即可. - 引入功能包后,就可以在內(nèi)存中動(dòng)態(tài)構(gòu)建子類
- 代理的類不能為
final
,否則報(bào)錯(cuò) - 目標(biāo)對(duì)象的方法如果為
final/static
,那么就不會(huì)被攔截,即不會(huì)執(zhí)行目標(biāo)對(duì)象額外的業(yè)務(wù)方法.
基本使用
- <!-- https://mvnrepository.com/artifact/cglib/cglib -->
- <dependency>
- <groupId>cglib</groupId>
- <artifactId>cglib</artifactId>
- <version>2.2</version>
- </dependency>
XML
方法攔截器
- public class LogInterceptor implements MethodInterceptor{
-
- /*
- * @param o 要進(jìn)行增強(qiáng)的對(duì)象
- * @param method 要攔截的方法
- * @param objects 參數(shù)列表,基本數(shù)據(jù)類型需要傳入其包裝類
- * @param methodProxy 對(duì)方法的代理,
- * @return 執(zhí)行結(jié)果
- * @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()));
- }
- }
Java
測試用例
- public class CglibMain {
-
- public static void main(String[] args) {
- // 創(chuàng)建Enhancer對(duì)象,類似于JDK動(dòng)態(tài)代理的Proxy類
- Enhancer enhancer = new Enhancer();
- // 設(shè)置目標(biāo)類的字節(jié)碼文件
- enhancer.setSuperclass(UserDao.class);
- // 設(shè)置回調(diào)函數(shù)
- enhancer.setCallback(new LogInterceptor());
- // create會(huì)創(chuàng)建代理類
- UserDao userDao = (UserDao)enhancer.create();
- userDao.update();
- userDao.select();
- }
- }
Java
結(jié)果
- 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動(dòng)態(tài)代理與CGLIB動(dòng)態(tài)代理對(duì)比
JDK 動(dòng)態(tài)代理
- 為了解決靜態(tài)代理中,生成大量的代理類造成的冗余;
JDK
動(dòng)態(tài)代理只需要實(shí)現(xiàn)InvocationHandler
接口,重寫invoke
方法便可以完成代理的實(shí)現(xiàn),- jdk的代理是利用反射生成代理類
Proxyxx.class
代理類字節(jié)碼,并生成對(duì)象 - jdk動(dòng)態(tài)代理之所以只能代理接口是因?yàn)榇眍惐旧硪呀?jīng)
extends
了Proxy
,而java是不允許多重繼承的,但是允許實(shí)現(xiàn)多個(gè)接口
優(yōu)點(diǎn):解決了靜態(tài)代理中冗余的代理實(shí)現(xiàn)類問題。
缺點(diǎn):JDK
動(dòng)態(tài)代理是基于接口設(shè)計(jì)實(shí)現(xiàn)的,如果沒有接口,會(huì)拋異常。
CGLIB 代理
- 由于
JDK
動(dòng)態(tài)代理限制了只能基于接口設(shè)計(jì),而對(duì)于沒有接口的情況,JDK
方式解決不了; CGLib
采用了非常底層的字節(jié)碼技術(shù),其原理是通過字節(jié)碼技術(shù)為一個(gè)類創(chuàng)建子類,并在子類中采用方法攔截的技術(shù)攔截所有父類方法的調(diào)用,順勢織入橫切邏輯,來完成動(dòng)態(tài)代理的實(shí)現(xiàn)。- 實(shí)現(xiàn)方式實(shí)現(xiàn)
MethodInterceptor
接口,重寫intercept
方法,通過Enhancer
類的回調(diào)方法來實(shí)現(xiàn)。 - 但是
CGLib
在創(chuàng)建代理對(duì)象時(shí)所花費(fèi)的時(shí)間卻比JDK多得多,所以對(duì)于單例的對(duì)象,因?yàn)闊o需頻繁創(chuàng)建對(duì)象,用CGLib
合適,反之,使用JDK
方式要更為合適一些。 - 同時(shí),由于
CGLib
由于是采用動(dòng)態(tài)創(chuàng)建子類的方法,對(duì)于final
方法,無法進(jìn)行代理。
優(yōu)點(diǎn):沒有接口也能實(shí)現(xiàn)動(dòng)態(tài)代理,而且采用字節(jié)碼增強(qiáng)技術(shù),性能也不錯(cuò)。
缺點(diǎn):技術(shù)實(shí)現(xiàn)相對(duì)難理解些。
作者:柯廣的網(wǎng)絡(luò)日志
微信公眾號(hào):Java大數(shù)據(jù)與數(shù)據(jù)倉庫