運行時類型識別(RTTI, Run-Time Type Information)是Java中非常有用的機制,在java中,有兩種RTTI的方式,一種是傳統的,即假設在編譯時已經知道了所有的類型;還有一種,是利用反射機制,在運行時再嘗試確定類型信息。
本篇博文會結合Thinking in Java 的demo 和實際開發中碰到的例子,對Java反射和獲取類型信息做總體上整理。文章主要分為三塊:
- Java類加載和初始化
- Java中RTTI
- Java利用反射獲取運行時類型信息
一:Java類加載和初始化
在學習RTTI的時候,首先需要知道Java中類是如何加載的,java又是如何根據這些class文件得到JVM中需要的信息(備注:我在此處實在是想不到更好的描述,望讀者可以給出更好的描述)
1.1 類加載器(類加載的工具)
類加載器子系統包含一條加載器鏈,只有一個“原生的類加載器”他是jvm實現的一部分,可以用來記載本地jar包內的class,若涉及加載網絡上的類,或者是web服務器應用,可以掛接額外的類加載器。
1.2 Java使用一個類所需的准備工作
1.2.1 動態加載
所有的類都是第一次使用的時候,動態加載到JVM中。創建對類的靜態成員的引用,加載這個類。Java程序在開始運行的時候並非完全加載,類都是用的地方在加載,這就是動態加載
①:首先檢查這個類是否被加載
②:如果沒有加載,再去根據類名查找.class文件,加載類的字節碼,並校驗是否存在不良代碼,
測試代碼如下:
//candy.java public class Candy { static { System.out.println("loading Candy"); } } //cookie.java public class Cookie { static { System.out.println("loading Cookie"); } } //Gum.java public class Gum { static { System.out.println("loading Gum"); } } //TestMain.java public class TestMain { public static void main(String[] args) { System.out.println("inside main"); new Candy(); System.out.println("After create Candy"); try { Class.forName("com.RuntimeTypeInformation.Gum"); } catch (ClassNotFoundException e) { System.out.println("Could not find Class"); } System.out.println("After Class.forName"); new Cookie(); System.out.println("After new Cookie()"); } static void printClassInfo(Class c){ System.out.println("Class Name :"+c.getName() +"is interface? :" + c.isInterface() +"simple Name "+ c.getSimpleName() ); }
從輸出結果可以清楚看到;class對象僅在需要的時候才會加載,static初始化是在類加載的時候進行
1.2.2 鏈接
驗證類中的字節碼,為靜態域分配存儲空間。如果必須的話,將解析這個類創建的對其他類的所有引用
1.2.3 初始化
如果該類存在超類,對其初始化,執行靜態初始化器和靜態代碼塊。初始化延遲至 對靜態方法或者非靜態方法首次引用時執行
二:Java中RTTI
2.1 :為什么要用到運行時類型信息(就是RTTI)
實際開發中,需求並不是一成不變的(准確來說是經常變),而每新添加需求如果代碼的改動量越小肯定是越能提高效率。比如:
package com.RuntimeTypeInformation.circle; import java.util.Arrays; import java.util.List; abstract class Shape { void draw(){ System.out.println(this+".draw()"); } abstract public String toString(); } class Circle extends Shape{ @Override public String toString() { return "Circle"; } } class Triangle extends Shape{ @Override public String toString() { return "Triangle"; } } public class Shapes{ public static void main(String[] args) { //題外話,Arrays.asList 可變參數列表,可以把傳入的多個對象轉為一個list List<Shape> shapes = Arrays.asList(new Triangle(),new Circle()); for (Shape shape : shapes) { shape.draw(); } } }
當我想要添加一個新的形狀,比如說長方形,我只需要編寫一個新類繼承Shape即可,而不需要修改調用的地方 。在這里用到了 ”多態“(雖然調用的都是shpe的方法,但是JVM能在運行期
准確的知道應該調用具體哪個子類的方法)
當你第一次了解"多態",你可能是簡單知道墮胎就是這么一回事,那么,現在我們去研究一下,java是怎樣處理的.
① 當把Triangle,Circle 放到 List<Shape>時,會向上轉型為Shape,丟失具體的類型
② 當從容器中取出Shape對象的時候,List內實際存放的是Object, 在運行期自動將結果轉為Shape,這就是RTTI的工作( 在運行時識別一個對象的類型)
這時候,如果客戶需求又改了,說不希望畫的結果存在圓形。應對這種需求,我們可以采用RTTI 查詢某個shape引用所指向的具體類型(具體怎么用,可以接着往下看)
2.2 :RTTI在運行時如何表示
Java的核心思想就是:”一切皆是對象“,比如我們對形狀抽象,得到圓形類,三角形類。但我們 對這些類在做一次抽象,得到class用於描述類的一般特性
上圖是我用畫圖畫的(有點撈見諒),如果我們可以拿到對象的class,我們就可以利用RTTI得到具體的java類。至於如何拿到Class和怎樣用Class得到准確的類,繼續往下看。
2.3 : Class對象
每一個類都存在與之對應的Class對象(保存在.class文件中),根據class得到具體的對象,請參考“第一章節 類的加載和初始化”
2.3.1 Class對象獲取的方式
①:Class.forName("全限定類名"),得到Class對象,副作用是“如果對應的類沒有加載,則會加載類”。找不到會拋出“”ClassNotFoundException”
②:如果有對象,可以直接用對象得到與之對應的Class對象 比如
Shape shape = new Circle(); shape.getClass()
③ ;通過類字面常量 : Shape.class.推薦用該方法,第一是編譯器會做檢查,第二是根除了對forName的調用,提高效率
2.3.2: Class對象的常用方法
方法名 | 說明 |
---|---|
forName() | (1)獲取Class對象的一個引用,但引用的類還沒有加載(該類的第一個對象沒有生成)就加載了這個類。 (2)為了產生Class引用,forName()立即就進行了初始化。 |
Object-getClass() | 獲取Class對象的一個引用,返回表示該對象的實際類型的Class引用。 |
getName() | 取全限定的類名(包括包名),即類的完整名字。 |
getSimpleName() | 獲取類名(不包括包名) |
getCanonicalName() | 獲取全限定的類名(包括包名) |
isInterface() | 判斷Class對象是否是表示一個接口 |
getInterfaces() | 返回Class對象數組,表示Class對象所引用的類所實現的所有接口。 |
getSupercalss() | 返回Class對象,表示Class對象所引用的類所繼承的直接基類。應用該方法可在運行時發現一個對象完整的繼承結構。 |
newInstance() | 返回一個Oject對象,是實現“虛擬構造器”的一種途徑。使用該方法創建的類,必須帶有無參的構造器。 |
getFields() | 獲得某個類的所有的公共(public)的字段,包括繼承自父類的所有公共字段。 類似的還有getMethods和getConstructors。 |
getDeclaredFields | 獲得某個類的自己聲明的字段,即包括public、private和proteced,默認但是不包括父類聲明的任何字段。類似的還有getDeclaredMethods和getDeclaredConstructors。 |
2.3.3 泛化的Class
Class引用表示它所指向的對象的確切類型,java1.5之后,允許開發者對Class引用所指向的Class對象進行限定,也就是添加泛型。
public static void main(String[] args) { Class<Integer> intclass = int.class; intclass = Integer.class; }
這樣可以在編譯器進行類型檢查,當然可以通過 “通配符” 讓引用泛型的時候放松限制 ,語法 : Class<?>
目的:
①:為了可以在編譯器就做類型檢查
② : 當 Class<Circle> circle = circle.getClass(); circle.newInstance() 會得到具體的類型 。但此處需注意:
public class Shapes{ public static void main(String[] args) throws InstantiationException, IllegalAccessException { Class<Circle> circles = Circle.class; Circle circle = circles.newInstance();//第一:泛化class.newInstance可以直接得到具體的對象 Class<? super Circle> shape = circles.getSuperclass(); Object shape1 = shape.newInstance();//第二:它的父類,只能用逆變的泛型class接收,newInstance得到的是Object類型 } }
2.3 : RTTI形式總結:
①:傳統的類型轉換,比如我們在上邊的demo中用到的 shape.draw();
②:利用Class,獲取運行時信息。
③:得到具體的對象
三:Java利用反射獲取運行時類型信息
如果不知道某一個對象引用的具體類型(比如已經上轉型的對象),RTTI可以得到。但前提是這個類型編譯器必須已知(那些是編譯期不可知呢? 磁盤文件或者是網絡連接中獲取一串代表類的字節碼)
跨網絡的遠程平台上提供創建和運行對象的能力 這被稱為 RMI(遠程方法調用),下面會具體的介紹一下 RMI的實現方式
反射提供了一種機制,用於檢查可用的方法,並返回方法名,調用方法。
3.1 : 獲取的方式
Java中提供了jar包 ,Java.lang.reflect 和Class對象一起對反射的概念提供支持。
3.1.1 Java.lang.reflect :
該類庫中包含了Field Method Constructor.這些類型的對象在JVM運行時創建,用於表示未知類里對應的成員。從而:
①:用Constructor創建對象,用get set讀取Field內的字段
②:用Method.invoke()調用方法
③:用getFields()、getMethods()、getConstuctors() 得到與之對應的數組
3.1.2 RTTI和RMI的區別
檢查對象,查看對象屬於哪個類,加載類的class文件
①:RTTI會在編譯期打開和檢查.class文件
②:RMI 在編譯期是 看不到.class文件。只能在運行期打開和檢查.class文件
3.2 : 動態代理
3.2.1 我假設你對“代理模式”存在一定的了解(還是簡單說一下,代理模式就是在接口和實現之前加一層,用於剝離接口的一些額外的操作)下面是代理模式的示例代碼:
public interface Subject { public void doSomething(); } public class RealSubject implements Subject { public void doSomething() { System.out.println( "call doSomething()" ); } } public class ProxyHandler implements InvocationHandler { private Object proxied; public ProxyHandler( Object proxied ) { this.proxied = proxied; } public Object invoke( Object proxy, Method method, Object[] args ) throws Throwable { //在轉調具體目標對象之前,可以執行一些功能處理 //轉調具體目標對象的方法 return method.invoke( proxied, args); //在轉調具體目標對象之后,可以執行一些功能處理 } }
3.2.2 動態代理就是:動態的創建代理並動態地處理對其所代理的方法的調用。可以參考 "徹底理解JAVA動態代理" ,"深度剖析JDK動態代理機制"。可以理解為更加靈活的代理模式
① 動態代理使用步驟:
1.通過實現InvocationHandler接口來自定義自己的InvocationHandler;
2.通過Proxy.getProxyClass獲得動態代理類
public class MyProxy { public interface IHello{ void sayHello(); } static class Hello implements IHello{ public void sayHello() { System.out.println("Hello world!!"); } } //自定義InvocationHandler static class HWInvocationHandler implements InvocationHandler{ //目標對象 private Object target; public HWInvocationHandler(Object target){ this.target = target; } public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("------插入前置通知代碼-------------"); //執行相應的目標方法 Object rs = method.invoke(target,args); System.out.println("------插入后置處理代碼-------------"); return rs; } } public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { //生成$Proxy0的class文件 System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true"); IHello ihello = (IHello) Proxy.newProxyInstance(IHello.class.getClassLoader(), //加載接口的類加載器 new Class[]{IHello.class}, //一組接口 new HWInvocationHandler(new Hello())); //自定義的InvocationHandler ihello.sayHello(); } }
② :動態代理的原理,列舉一下參考文獻把:(本質上還是用到了反射)
③ 動態代理應用以及備注說明 :
JDK實現動態代理需要實現類通過接口定義業務方法 (接下來我會簡單說一下Cglib實現動態代理)。第二是動態代理非常重要 是反射一個極其重要的模塊,很多框架都離不開動態代理,比如Spring 。所以,推薦讀者在多去研究一下。
④:Cglib實現動態代理
參考文檔: cglib動態代理介紹(一)
CGLIB是一個強大的高性能的代碼生成包。它廣泛的被許多AOP的框架使用,例如spring AOP和dynaop,為他們提供方法的interception(攔截)。最流行的OR Mapping工具hibernate也使用CGLIB來代理單端single-ended(多對一和一對一)關聯(對集合的延遲抓取,是采用其他機制實 現的)。EasyMock和jMock是通過使用模仿(moke)對象來測試Java代碼的包。它們都通過使用CGLIB來為那些沒有接口的類創建模仿 (moke)對象。
CGLIB包的底層是通過使用一個小而快的字節碼處理框架ASM,來轉換字節碼並生成新的類。除了CGLIB包,腳本語言例如 Groovy和BeanShell,也是使用ASM來生成java的字節碼。當不鼓勵直接使用ASM,因為它要求你必須對JVM內部結構包括class文 件的格式和指令集都很熟悉
"在運行期擴展java類及實現java接口",補充的是java動態代理機制要求必須實現了接口,而cglib針對沒實現接口的那些類,原理是通過繼承這些類,成為子類,覆蓋一些方法,所以cglib對final的類也不生效
cglib實現動態代理的demo:參考 CGLib動態代理原理及實現
這是要代理的類:
public class SayHello { public void say(){ System.out.println("hello everyone"); } }
代理類的核心
public class CglibProxy implements MethodInterceptor{ private Enhancer enhancer = new Enhancer(); public Object getProxy(Class clazz){ //設置需要創建子類的類 enhancer.setSuperclass(clazz); enhancer.setCallback(this); //通過字節碼技術動態創建子類實例 return enhancer.create(); } //實現MethodInterceptor接口方法 public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { System.out.println("前置代理"); //通過代理類調用父類中的方法 Object result = proxy.invokeSuper(obj, args); System.out.println("后置代理"); return result; } }
測試結果:
public class DoCGLib { public static void main(String[] args) { CglibProxy proxy = new CglibProxy(); //通過生成子類的方式創建代理類 SayHello proxyImp = (SayHello)proxy.getProxy(SayHello.class); proxyImp.say(); } }
四: Java反射在實際開發中應用
通常,我們在一般的業務需求中是用不到反射的,但我們在更加動態的代碼時,我們就可以選擇反射來實現(例如對象序列化和 JavaBean)。主要的邏輯我在上邊都已經說明了,所以接下來 更多的是代碼展示:
實際開發中,在運行時得到Class信息,獲取method ,通過反射method.invoke()調用方法。這樣做是出於AOP的設計思想。舉例來說,我一個傳統的web項目,我可以同過http直接傳遞請求給后台servlet,假如我想添加一個記錄日志,或者是在請求的session中添加一個信息,如果只有一個請求,我可以直接在htttp加,但實際上請求會很多,這是我為什么在sevlet外在抽出一層,通過反射調用servlet
當然,很多框架其實也為我們提供了攔截的配置(這是后話)
4.1 :在web項目中創建統一的攔截層
doPost(..){ //這是項目中的setvlet統一的攔截層,接下來我們看一下 actionInvoker.invoke ... else if (requestType.equalsIgnoreCase("image")) { try { ActionClassInfo actionClassInfo = actionInvoker.getClassInfo(action, request, response); actionClassInfo.setArgs(queryStringMap); Object object = actionInvoker.invoke(actionClassInfo); response.addHeader("accept-ranges", "bytes"); byte[] bytes = (byte[]) object; response.addHeader("Content-type", "application/png"); response.addHeader("content-length", String.valueOf(bytes.length)); response.getOutputStream().write(bytes, 0, bytes.length); } catch (Exception e) { e.printStackTrace(); } catch (Throwable e) { e.printStackTrace(); } finally { response.getOutputStream().flush(); response.getOutputStream().close(); } } }
actionInvoker.invoke()方法代碼如下: 在這方法內,我就可以添加我想要的處理,比如先判斷是否在緩存中存在,核心的只有 method.invoke
public Object invoke(ActionClassInfo action) throws Exception { // 執行方法之前 Object cache = null; for (Object object : action.getProxys()) { if (object instanceof Intercepter){ cache = ((Intercepter) object).before(action); if(cache != null && object instanceof RedisCacheHandler){ return cache; //緩存的結果直接返回 } } } Method method = action.getMethod(); Object business = action.getClazz(); Map<Object, Object> args = action.getArgs(); method.setAccessible(true); Object result = method.invoke(business, args); // 執行方法后 for (Object object : action.getProxys()) { if (object instanceof Intercepter) result = ((Intercepter) object).after(result, action); } return result; }
4.2 : 用於webService服務 :和servlet做同意攔截,用反射去調用方法的目的一樣(添加一些想要的處理,比如校驗用戶)。核心也是反射調用方法