黑馬程式設計師-----基礎加強-動態代理
------<a href="http://www.itheima.com" target="blank">Java培訓、Android培訓、iOS培訓、.Net培訓</a>、期待與您交流! -------
代理類的作用與原理
1、生活中的代理:就是常說的代理商,從廠商將商品賣給消費者,消費者不用很麻煩的到廠商在購買了。
2、程式中的代理:要為已經存在的多個具有相同介面的目標類的各個方法增加一些系統功能,如異常處理、日誌、計算方法的執行時間、事物管理等等。
3、簡單示例:編寫一個與目標類具有相同介面的代理類,代理類的每個方法呼叫目標類的相同方法,並在呼叫方法時加上系統功能的程式碼,如:
目標類:代理類:
class X{ Xproxy{
void sayHello(){ void sayHello(){
syso:Hello; startTime
} X. sayHello();
} endTime;}}
一般用介面來引用其子類,如:Collection coll = new ArrayList();
4、代理類的有點:
如果採用工廠模式和配置檔案的方式進行管理,則不需要修改客戶端程式,在配置檔案中配置是使用目標類還是代理類。這樣以後很容易切換,如果想要日誌功能時,就配置代理類,否則配置目標類,這樣,增加系統功能很容易,以後執行一段時間後,又想換掉系統功能也很容易。
5、獲取代理類的構造方法和方法程式碼
import java.lang.reflect.*; import java.util.*; public class ProxyTest { public static void main(String[] args) throws Exception{ //獲取代理類Proxy的Class物件,傳入的是類載入器和相應的位元組碼物件 Class clazzProxy1 = Proxy.getProxyClass( Collection.class.getClassLoader(), Collection.class); System.out.println(clazzProxy1.getName());//$Proxy0 System.out.println("---begin constructor list------"); //獲取代理類的構造方法,可能含有多個,得到陣列 Constructor[] constructors = clazzProxy1.getConstructors(); //遍歷陣列,獲取每個構造方法 for(Constructor constructor : constructors){ //先得到構造方法的名字,並裝入字串容器中 String name = constructor.getName(); StringBuilder sBul = new StringBuilder(name); sBul.append('('); //獲取構造方法中的引數型別,並遍歷 Class[] clazzParams = constructor.getParameterTypes(); for(Class clazzParam : clazzParams){ sBul.append(clazzParam.getName()).append(','); } //將最後一個逗號去除 if(clazzParams != null && clazzParams.length!=0) sBul.deleteCharAt(sBul.length()-1); sBul.append(')'); System.out.println(sBul.toString()); } System.out.println("---begin method list------"); //獲取代理類的方法,存入陣列 Method[] methods = clazzProxy1.getMethods(); //遍歷陣列,獲取每個方法 for(Method method : methods){ //先得到方法的名字,並裝入字串容器中 String name = method.getName(); StringBuilder sBul = new StringBuilder(name); sBul.append('('); //獲取方法中的引數型別,並遍歷 Class[] clazzParams = method.getParameterTypes(); for(Class clazzParam : clazzParams){ sBul.append(clazzParam.getName()).append(','); } //將最後一個逗號去除 if(clazzParams!=null && clazzParams.length!=0) sBul.deleteCharAt(sBul.length()-1); sBul.append(')'); System.out.println(sBul.toString()); } }
AOP
簡述:AOP(Aspect Oriented Program)即面向方面的程式設計。
系統中存在交叉業務,一個交叉業務就是要切入到系統中的一個方面,如下所示:
安全 事務 日誌
StudentService ------|----------|------------|-------------
CourseService ------|----------|------------|-------------
MiscService ------|----------|------------|-------------
用具體的程式程式碼描述交叉業務:
method1 method2 method3
{ { {
------------------------------------------------------切面
.... .... ......
------------------------------------------------------切面
} } }
交叉業務的程式設計問題即為面向方面的程式設計(Aspect oriented program ,簡稱AOP),AOP的目標就是要使交叉業務模組化。可以採用將切面程式碼移動到原始方法的周圍,這與直接在方法中編寫切面程式碼的執行效果是一樣的,如下所示:
------------------------------------------------------切面
func1 func2 func3
{ { {
.... .... ......
} } }
------------------------------------------------------切面
使用代理技術正好可以解決這種問題,代理是實現 AOP 功能的核心和關鍵技術。
動態代理技術
一、概述:
1、要為系統中的各種介面的類增加代理功能,那將需要太多的代理類,這時就不能採用靜態代理方式,需用動態代理技術。
2、動態代理類:JVM可在執行時,動態生成類的位元組碼,這種動態(不是代理,只是拿出來作為代理類)生成的類往往被用作代理類,即動態代理類。
注:JVM生成的動態類必須實現一或多個介面,所以JVM生成的動態代理類只能用作具有相同介面的目標類代理。
3、CGLIB庫可以動態生成一個類的子類,一個類的子類也可以作為該類的代理,所以,如果要為一個沒有實現介面的類生成動態代理,那麼可以使用CGLIB庫。
4、代理類各個方法通常除了呼叫目標相應方法和對外返回目標返回的結果外,還可以再代理方法中的如下位置上加上系統功能程式碼:
1)在呼叫目標方法之前
2)在呼叫目標方法之後
3)在呼叫目標方法前後
4)在處理目標方法異常的catch塊中。
二、分析JVM動態生成的類
1、建立動態類的例項物件:
1)用反射獲得構造方法
2)編寫一個最簡單的InvocationHandler的類
3)呼叫構造方法建立動態類的例項物件,並將編寫的InvocationHandler類的例項物件傳進去。
示例:
第一、列印建立的物件和呼叫物件的無返回值的方法和getClass方法,演示呼叫其他沒有返回值的方法報告的異常
第二、將建立的動態類的例項物件的代理改寫成為匿名內部類的形式編寫。
package cn.itcast.test3;
import java.lang.reflect.*;
import java.util.*;
public class ProxyTest {
public static void main(String[] args) throws Exception{
//建立動態代理類的三種方式
//方式一:通過介面的子類建立物件
Collection proxy1 = (Collection)
constructor.newInstance(new MyInvocationHandler());
System.out.println(proxy1);//null
System.out.println(proxy1.toString());//null
proxy1.clear();//無異常
//proxy1.size();
//方式二:匿名內部類
Collection proxy2 = (Collection)
constructor.newInstance(new InvocationHandler(){
public Object invoke(Object proxy, Method method,
Object[] args) throws Throwable {
// TODO Auto-generated method stub
return null;
}
});
//方式三:
//通過代理類的newProxyInstance方法直接建立物件
Collection proxy3 = (Collection)Proxy.newProxyInstance(
//定義代理類的類載入器
Collection.class.getClassLoader(),
//代理類要實現的介面列表
new Class[]{Collection.class},
//指派方法呼叫的呼叫處理程式
new InvocationHandler() {
//建立集合,制定一個目標
ArrayList target = new ArrayList();
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
//測試程式執行時間
long beginTime = System.currentTimeMillis();
//呼叫目標方法,將其從return抽出來,加入代理所需的程式碼
Object retVal = method.invoke(target, args);
long endTime = System.currentTimeMillis();
//測試
System.out.println(method.getName() +
" run time of " +
(endTime - beginTime));
return retVal;
}
}
);
//通過代理類呼叫目標方法,每呼叫一個目標的方法就會執行代理類的方法
//當呼叫一次add方法時,就會找一次InvocationHandler這個引數的invoke方法
proxy3.add("sdfd");
proxy3.add("shrt");
proxy3.add("rtbv");
System.out.println(proxy3.size());
System.out.println(proxy3.getClass().getName());
}
}
讓jvm建立動態類及其例項物件,需要給它提供哪些資訊:
- 生成的類中有哪些方法,通過讓其實現哪些介面的方式進行告知;
- 產生的類位元組碼必須有個一個關聯的類載入器物件;
- 生成的代理類中的方法的程式碼是怎樣的,也得由我們提供。把我們的程式碼寫在一個約定好了介面物件的方法中,把物件傳給它,它呼叫我的方法,即相當於插入了我的程式碼。提供執行程式碼的物件就是那個 InvocationHandler 物件,它是在建立動態類的例項物件的構造方法時傳遞進去的。在上面的 InvocationHandler 物件的 invoke 方法中加一點程式碼,就可以看到這些程式碼被呼叫運行了。
三、問題:
1、在上面的方式一的程式碼中,呼叫無返回值的方法返回的是null,而呼叫有返回值方法的時候會報NullPointException異常的原因:
在proxy1.size()方法中,size()會呼叫Object invoke()方法,而對其覆寫時的返回值是null,而size()本身會返回int型別,兩者返回的型別不相等,所以就會報錯。
而對於proxy3.size()不報錯,是因為size()返回值與代理物件中handler引數返回值是一致的,當前目標返回什麼,代理就返回什麼,所以不會報錯。
注意:目標返回值和代理返回值必須是同一型別。
2、為何動態類的例項物件的getClass()方法返回了正確結果,而沒呼叫invoke方法:
因為代理類從Object上繼承了許多方法,其中只對三個方法(hansCode、equals和toString)進行開發,委託給handler去自行處理,對於它身上其他方法不會交給代理類去實現,所以對於getClass()方法,還是會有Object本身實現的。即proxy3.getClass(),該是什麼結果還是什麼結果,並不會交給invoke方法處理。
四、總結分析動態代理類的統計原理和結構:
動態代理的工作原理圖
1、怎樣將目標傳進去:
1)直接在InvocationHandler實現類中建立目標類的例項物件,可看執行效果和加入日誌程式碼,但是毫無意義。
2)為InvocationHandler實現類注入目標的例項物件,不能採用匿名內部類的形式了。
3)讓匿名內的InvocationHandler實現類訪問外面的方法中的目標類例項物件的final型別的引用變數。
2、動態代理的工作原理:
1)Client(客戶端)呼叫代理,代理的構造方法接受一個InvocationHandler,client呼叫代理的各個方法,代理的各個方法請求轉發給剛才通過構造方法傳入的handler物件,又把各請求分發給目標的相應的方法。就是將handler封裝起來,其中this引用了當前的放(發來什麼請求就接受哪個方法)。
2)將建立代理的過程改為一種更優雅的方式,eclipse重構出一個getProxy方法繫結接受目標,同時返回代理帝鄉,讓呼叫者更懶惰,更方便,呼叫者甚至不用接觸任何代理的API。
3、把系統功能代理模組化,即切面程式碼也改為通過引數形式提供,怎麼把要執行的系統功能程式碼以引數的形式提供:
1)把要執行的程式碼裝到一個物件的某個方法中,然後把此物件作為引數傳遞,接收者只要呼叫這個物件的方法,即等於執行了外接提供的程式碼。
2)為方法增加一個Advice引數。
程式碼示例:
//建立介面Advice
import java.lang.reflect.Method;
/*介面中需要實現四個方法
* 呼叫目標方法之前
* 呼叫目標方法之後
* 呼叫目標方法前後
* 在處理目標方法異常的catch塊中
*/
//這裡只列出兩個作為示例
public interface Advice {
void beforeMethod(Method method);
void afterMethod(Method method);
}
//建立實現Advice介面的子類
package cn.itcast.test3;
import java.lang.reflect.Method;
//實現Advice介面中方法的具體內容
public class MyAdvice implements Advice {
long beginTime = 0;
public void beforeMethod(Method method) {
// TODO Auto-generated method stub
System.out.println("從這裡開始");
beginTime = System.currentTimeMillis();
}
public void afterMethod(Method method) {
// TODO Auto-generated method stub
long endTime = System.currentTimeMillis();
System.out.println("從這裡結束");
System.out.println(method.getName() + " run time of " + (endTime-beginTime));
}
}
//封裝getProxy方法
package cn.itcast.test3;
import java.lang.reflect.*;
import java.util.*;
public class MyProxy {
public static void main(String[] args)throws Exception {
//建立目標物件,並進行操作測試
final ArrayList target = new ArrayList();
Collection proxy = (Collection)getProxy(target,new MyAdvice());
proxy.add("sdf");
proxy.add("wgcd");
proxy.add("hgwe");
System.out.println(proxy.size());
}
//作為一個通用的方法,就使用Object
//傳入一個目標,並傳入一個介面,此介面作為通訊的契約,才能呼叫額外的方法
private static Object getProxy(final Object target,final Advice advice) {
Object proxy = Proxy.newProxyInstance(
target.getClass().getClassLoader(),
//這裡的介面要和target實現相同的介面
target.getClass().getInterfaces(),
new InvocationHandler() {
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
//通過契約,使用其方法--before和after方法
advice.beforeMethod(method);
Object value = method.invoke(target, args);
advice.afterMethod(method);
return value;
}
}
);
return proxy;
}
}
實現類似 spring 的可配置的 AOP 框架
需求:要求在不修改 Client 程式碼的情況下,可以修改我們建立的物件的型別,並且配置是否建立目標功能的代理類物件,那麼我們可以選用 配置檔案 + 工廠模式 的方法。我們的需求在配置檔案中配置好以後,使用工廠類物件讀取配置檔案獲得我們要建立的物件的類的名字的字串,使用該字串來在工廠中使用反射來建立我們我們需要的例項。 現在我們的配置選項可以設定為我們要建立的物件要麼是一般目標類物件,要麼是目標代理類物件,建立目標代理類物件的話,還需要指定 目標類 和 Advice 介面的實現類。 配置檔案格式可以為:#xxxClassName=java.util.ArrayList
xxxClassName=cn.itcast.proxy.aopframework.ProxyFactoryBean
xxxClassName.target=java.util.ArrayList
xxxClassName.advice=java.util.ArrayList
其中 xxxClassName 可以是 Collection 這種我們 Client 程式中需要的物件的型別的名字
config.properties 檔案:
Collection=java.util.ArrayList
#Collection=cn.itcast.proxy.aopframework.ProxyFactoryBean
Collection.target=java.util.ArrayList
Collection.advice=cn.itcast.proxy.MyAdvice
定義一個bean工廠介面
定義一個bean工廠介面,專門負責生產指定的 javabean 的物件package cn.itcast.proxy.aopframework;
public interface BeanFactory {
Object getBean(String className) throws Exception;
}
定義一個 ConfigurableBeanFactory 類
定義一個 ConfigurableBeanFactory 類實現 bean 工廠,它是根據配置檔案產生 bean 類
package cn.itcast.proxy.aopframework;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
public class ConfigurableBeanFactory implements BeanFactory {
private Properties properties;
public ConfigurableBeanFactory(InputStream propertiesInputStream) {
// TODO Auto-generated constructor stub
properties = new Properties();
try {
properties.load(propertiesInputStream);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
@Override
public Object getBean(String className) throws Exception {
// 根據傳入的類名去配置檔案中拿到實際配置的類名
String configClassName = properties.getProperty(className);
Object beanObject = Class.forName(configClassName).newInstance();
// 如果配置的物件是 代理工廠類物件,那麼要返回就得是下面配置的目標和Advice組合形成的代理的物件
if (beanObject instanceof ProxyFactoryBean) {
ProxyFactoryBean proxyFactoryBeanObject = (ProxyFactoryBean) beanObject;
String target = properties.getProperty(className + ".target");
String advice = properties.getProperty(className + ".advice");
proxyFactoryBeanObject.setTarget(target);
proxyFactoryBeanObject.setAdvice(advice);
beanObject = proxyFactoryBeanObject.getBean(target);
}
return beanObject;
}
public static void main(String[] args) throws Exception {
InputStream propertiesInputStream = ConfigurableBeanFactory.class
.getResourceAsStream("config.properties");
BeanFactory beanFactory = new ConfigurableBeanFactory(
propertiesInputStream);
System.out.println(beanFactory.getBean("Collection").getClass().getName());
}
}
定義一個 ProxyFactoryBean 類
定義一個 ProxyFactoryBean 類它是一個產生 代理類物件的工廠,它本身就是 javabean ,而且實現了 BeanFactory 介面。
package cn.itcast.proxy.aopframework;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import cn.itcast.proxy.Advice;
public class ProxyFactoryBean implements BeanFactory{
private String target;
private String advice;
public ProxyFactoryBean() {
super();
}
public String getTarget() {
return target;
}
public void setTarget(String target) {
this.target = target;
}
public String getAdvice() {
return advice;
}
public void setAdvice(String advice) {
this.advice = advice;
}
@Override
public Object getBean(String className) throws Exception {
// TODO Auto-generated method stub
Object target = Class.forName(this.target).newInstance();
Advice advice = (Advice)Class.forName(this.advice).newInstance();
return createDynamicProxyInstance(target, advice);
}
private static Object createDynamicProxyInstance(final Object target, final Advice advice) {
Object proxyObject = Proxy.newProxyInstance(target.getClass()
.getClassLoader(), target.getClass().getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method,
Object[] args) throws Throwable {
// TODO Auto-generated method stub
// 1.在 target 功能之前新增代理的額外功能
advice.beforeMethod(method);
// 2.呼叫 target 功能
Object returnValue = method.invoke(target, args);
// 3.在 target 功能之後新增代理的額外功能
advice.afterMethod(method);
return returnValue;
}
});
return proxyObject;
}
}