Java 反射機制詳解

翡青的部落格發表於2016-01-24

動態語言

動態語言,是指程式在執行時可以改變其結構:新的函式可以被引進,已有的函式可以被刪除等在結構上的變化。比如眾所周知的ECMAScript(JavaScript)便是一個動態語言。除此之外如Ruby、Python等也都屬於動態語言,而C、C++等語言則不屬於動態語言。(引自: 百度百科)

var execString = "alert(Math.floor(Math.random()*10));";
eval(execString);

Class反射機制

  • 指的是可以於執行時載入,探知和使用編譯期間完全未知的類.
  • 程式在執行狀態中, 可以動態載入一個只有名稱的類, 對於任意一個已經載入的類,都能夠知道這個類的所有屬性和方法; 對於任意一個物件,都能呼叫他的任意一個方法和屬性;
  • 載入完類之後, 在堆記憶體中會產生一個Class型別的物件(一個類只有一個Class物件), 這個物件包含了完整的類的結構資訊,而且這個Class物件就像一面鏡子,透過這個鏡子看到類的結構,所以被稱之為:反射。

Instances of the class Class represent classes and interfaces in a running Java application. An enum is a kind of class and an annotation is a kind of interface. Every array also belongs to a class that is reflected as a Class object that is shared by all arrays with the same element type and number of dimensions(維度). The primitive Java types (boolean, byte, char, short, int, long, float, anddouble), and the keyword void are also represented as Class objects.

  • 每個類被載入進入記憶體之後,系統就會為該類生成一個對應的java.lang.Class物件,通過該Class物件就可以訪問到JVM中的這個類.

Class物件的獲取

  • 物件的getClass()方法;
  • 類的.class(最安全/效能最好)屬性;
  • 運用Class.forName(String className)動態載入類,className需要是類的全限定名(最常用).

從Class中獲取資訊

Class類提供了大量的例項方法來獲取該Class物件所對應的詳細資訊,Class類大致包含如下方法,其中每個方法都包含多個過載版本,因此我們只是做簡單的介紹,詳細請參考JDK文件

獲取類內資訊

獲取內容 方法簽名
構造器 Constructor<T> getConstructor(Class<?>... parameterTypes)
包含的方法 Method getMethod(String name, Class<?>... parameterTypes)
包含的屬性 Field getField(String name)
包含的Annotation <A extends Annotation> A getAnnotation(Class<A> annotationClass)
內部類 Class<?>[] getDeclaredClasses()
外部類 Class<?> getDeclaringClass()
所實現的介面 Class<?>[] getInterfaces()
修飾符 int getModifiers()
所在包 Package getPackage()
類名 String getName()
簡稱 String getSimpleName()

一些判斷類本身資訊的方法

判斷內容 方法簽名
註解型別? boolean isAnnotation()
使用了該Annotation修飾? boolean isAnnotationPresent(Class<? extends Annotation> annotationClass)
匿名類? boolean isAnonymousClass()
陣列? boolean isArray()
列舉? boolean isEnum()
原始型別? boolean isPrimitive()
介面? boolean isInterface()
obj是否是該Class的例項 boolean isInstance(Object obj)

使用反射生成並操作物件:

Method Constructor Field這些類都實現了java.lang.reflect.Member介面,程式可以通過Method物件來執行相應的方法,通過Constructor物件來呼叫對應的構造器建立例項,通過Filed物件直接訪問和修改物件的成員變數值.

建立物件

通過反射來生成物件的方式有兩種:

  • 使用Class物件的newInstance()方法來建立該Class物件對應類的例項(這種方式要求該Class物件的對應類有預設構造器).
  • 先使用Class物件獲取指定的Constructor物件, 再呼叫Constructor物件的newInstance()方法來建立該Class物件對應類的例項(通過這種方式可以選擇指定的構造器來建立例項).

通過第一種方式來建立物件比較常見, 像Spring這種框架都需要根據配置檔案(如applicationContext.xml)資訊來建立Java物件,從配置檔案中讀取的只是某個類的全限定名字串,程式需要根據該字串來建立對應的例項,就必須使用預設的構造器來反射物件.
下面我們就模擬Spring實現一個簡單的物件池, 該物件池會根據檔案讀取key-value對, 然後建立這些物件, 並放入Map中.

配置檔案

{
  "objects": [
    {
      "id": "id1",
      "class": "com.fq.domain.User"
    },
    {
      "id": "id2",
      "class": "com.fq.domain.Bean"
    }
  ]
}

ObjectPool

/**
 * Created by jifang on 15/12/31.
 */
public class ObjectPool {

    private Map<String, Object> pool;

    private ObjectPool(Map<String, Object> pool) {
        this.pool = pool;
    }

    private static Object getInstance(String className) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        return Class.forName(className).newInstance();
    }

    private static JSONArray getObjects(String config) throws IOException {
        Reader reader = new InputStreamReader(ClassLoader.getSystemResourceAsStream(config));
        return JSONObject.parseObject(CharStreams.toString(reader)).getJSONArray("objects");
    }

    // 根據指定的JSON配置檔案來初始化物件池
    public static ObjectPool init(String config) {
        try {
            JSONArray objects = getObjects(config);
            ObjectPool pool = new ObjectPool(new HashMap<String, Object>());
            if (objects != null && objects.size() != 0) {
                for (int i = 0; i < objects.size(); ++i) {
                    JSONObject object = objects.getJSONObject(i);
                    if (object == null || object.size() == 0) {
                        continue;
                    }
                    String id = object.getString("id");
                    String className = object.getString("class");

                    pool.putObject(id, getInstance(className));
                }
            }
            return pool;
        } catch (IOException | ClassNotFoundException | InstantiationException | IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }

    public Object getObject(String id) {
        return pool.get(id);
    }

    public void putObject(String id, Object object) {
        pool.put(id, object);
    }

    public void clear() {
        pool.clear();
    }
}

Client

public class Client {

    @Test
    public void client() {
        ObjectPool pool = ObjectPool.init("config.json");
        User user = (User) pool.getObject("id1");
        System.out.println(user);

        Bean bean = (Bean) pool.getObject("id2");
        System.out.println(bean);
    }
}

User

public class User {

    private int id;
    private String name;
    private String password;

    public int getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + '/'' +
                ", password='" + password + '/'' +
                '}';
    }
}

Bean

public class Bean {
    private Boolean usefull;
    private BigDecimal rate;
    private String name;

    public Boolean getUsefull() {
        return usefull;
    }

    public void setUsefull(Boolean usefull) {
        this.usefull = usefull;
    }

    public BigDecimal getRate() {
        return rate;
    }

    public void setRate(BigDecimal rate) {
        this.rate = rate;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Bean{" +
                "usefull=" + usefull +
                ", rate=" + rate +
                ", name='" + name + '/'' +
                '}';
    }
}

注意: 需要在pom.xml中新增如下依賴:

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.7</version>
</dependency>

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>18.0</version>
</dependency>

呼叫方法

當獲取到某個類對應的Class物件之後, 就可以通過該Class物件的getMethod來獲取一個Method陣列或Method物件.每個Method物件對應一個方法,在獲得Method物件之後,就可以通過呼叫invoke方法來呼叫該Method物件對應的方法.

@CallerSensitive
public Object invoke(Object obj, Object... args)
    throws IllegalAccessException, IllegalArgumentException,
       InvocationTargetException
{
    ...
}

下面我們對上面的物件池加強:可以看到Client獲取到的物件的成員變數全都是預設值,既然我們已經使用了JSON這麼優秀的工具,我們又學習了動態呼叫物件的方法,那麼我們就通過配置檔案來給物件設定值(在物件建立時), 新的配置檔案形式如下:

{
  "objects": [
    {
      "id": "id1",
      "class": "com.fq.domain.User",
      "fields": [
        {
          "name": "id",
          "value": 101
        },
        {
          "name": "name",
          "value": "feiqing"
        },
        {
          "name": "password",
          "value": "ICy5YqxZB1uWSwcVLSNLcA=="
        }
      ]
    },
    {
      "id": "id2",
      "class": "com.fq.domain.Bean",
      "fields": [
        {
          "name": "usefull",
          "value": true
        },
        {
          "name": "rate",
          "value": 3.14
        },
        {
          "name": "name",
          "value": "bean-name"
        }
      ]
    },
    {
      "id": "id3",
      "class": "com.fq.domain.ComplexBean",
      "fields": [
        {
          "name": "name",
          "value": "complex-bean-name"
        },
        {
          "name": "refBean",
          "ref": "id2"
        }
      ]
    }
  ]
}

其中fields代表該Bean所包含的屬性, name為屬性名稱, value為屬性值(屬性型別為JSON支援的型別), ref代表引用一個物件(也就是屬性型別為Object,但是一定要引用一個已經存在了的物件)

/**
 * @author jifang
 * @since 15/12/31下午4:00
 */
public class ObjectPool {

    private Map<String, Object> pool;

    private ObjectPool(Map<String, Object> pool) {
        this.pool = pool;
    }

    private static JSONArray getObjects(String config) throws IOException {
        Reader reader = new InputStreamReader(ClassLoader.getSystemResourceAsStream(config));
        return JSONObject.parseObject(CharStreams.toString(reader)).getJSONArray("objects");
    }

    private static Object getInstance(String className, JSONArray fields)
            throws ClassNotFoundException, NoSuchMethodException,
            IllegalAccessException, InstantiationException, InvocationTargetException {

        // 配置的Class
        Class<?> clazz = Class.forName(className);
        // 目標Class的例項物件
        Object targetObject = clazz.newInstance();
        if (fields != null && fields.size() != 0) {
            for (int i = 0; i < fields.size(); ++i) {
                JSONObject field = fields.getJSONObject(i);
                // 需要設定的成員變數名
                String fieldName = field.getString("name");

                // 需要設定的成員變數的值
                Object fieldValue;
                if (field.containsKey("value")) {
                    fieldValue = field.get("value");
                } else if (field.containsKey("ref")) {
                    String refBeanId = field.getString("ref");
                    fieldValue = OBJECTPOOL.getObject(refBeanId);
                } else {
                    throw new RuntimeException("neither value nor ref");
                }

                String setterName = "set" +
                        fieldName.substring(0, 1).toUpperCase() +
                        fieldName.substring(1);
                // 需要設定的成員變數的setter方法
                Method setterMethod = clazz.getMethod(setterName, fieldValue.getClass());
                // 呼叫setter方法將值設定進去
                setterMethod.invoke(targetObject, fieldValue);
            }
        }

        return targetObject;
    }

    private static ObjectPool OBJECTPOOL;

    // 建立一個物件池的例項(保證是多執行緒安全的)
    private static void initSingletonPool() {
        if (OBJECTPOOL == null) {
            synchronized (ObjectPool.class) {
                if (OBJECTPOOL == null) {
                    OBJECTPOOL = new ObjectPool(new ConcurrentHashMap<String, Object>());
                }
            }
        }
    }

    // 根據指定的JSON配置檔案來初始化物件池
    public static ObjectPool init(String config) {
        // 初始化pool
        initSingletonPool();

        try {
            JSONArray objects = getObjects(config);
            for (int i = 0; objects != null && i < objects.size(); ++i) {
                JSONObject object = objects.getJSONObject(i);
                if (object == null || object.size() == 0) {
                    continue;
                }
                String id = object.getString("id");
                String className = object.getString("class");

                // 初始化bean並放入池中
                OBJECTPOOL.putObject(id, getInstance(className, object.getJSONArray("fields")));
            }
            return OBJECTPOOL;
        } catch (IOException | ClassNotFoundException |
                InstantiationException | IllegalAccessException |
                NoSuchMethodException | InvocationTargetException e) {
            throw new RuntimeException(e);
        }
    }

    public Object getObject(String id) {
        return pool.get(id);
    }

    public void putObject(String id, Object object) {
        pool.put(id, object);
    }

    public void clear() {
        pool.clear();
    }
}

Client

public class Client {

    @Test
    public void client() {
        ObjectPool pool = ObjectPool.init("config.json");
        User user = (User) pool.getObject("id1");
        System.out.println(user);

        Bean bean = (Bean) pool.getObject("id2");
        System.out.println(bean);

        ComplexBean complexBean = (ComplexBean) pool.getObject("id3");
        System.out.println(complexBean);
    }
}

ComplexBean

public class ComplexBean {

    private String name;

    private Bean refBean;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Bean getRefBean() {
        return refBean;
    }

    public void setRefBean(Bean refBean) {
        this.refBean = refBean;
    }

    @Override
    public String toString() {
        return "ComplexBean{" +
                "name='" + name + '/'' +
                ", refBean=" + refBean +
                '}';
    }
}

Spring框架就是通過這種方式將成員變數值以及依賴物件等都放在配置檔案中進行管理的,從而實現了較好地解耦(不過Spring是通過XML作為配置檔案).

訪問成員變數

通過Class物件的的getField()方法可以獲取該類所包含的全部或指定的成員變數Field,Filed提供瞭如下兩組方法來讀取和設定成員變數值.

  • getXxx(Object obj): 獲取obj物件的該成員變數的值, 此處的Xxx對應8中基本型別,如果該成員變數的型別是引用型別, 則取消get後面的Xxx;
  • setXxx(Object obj, Xxx val): 將obj物件的該成員變數值設定成val值.此處的Xxx對應8種基本型別, 如果該成員型別是引用型別, 則取消set後面的Xxx;

注: getDeclaredXxx方法可以獲取所有的成員變數,無論private/public;

/**
 * @author jifang
 * @since 16/1/2下午1:00.
 */
public class Client {

    @Test
    public void client() throws NoSuchFieldException, IllegalAccessException {
        User user = new User();
        Field idFiled = User.class.getDeclaredField("id");
        setAccessible(idFiled);
        idFiled.setInt(user, 46);

        Field nameFiled = User.class.getDeclaredField("name");
        setAccessible(nameFiled);
        nameFiled.set(user, "feiqing");

        Field passwordField = User.class.getDeclaredField("password");
        setAccessible(passwordField);
        passwordField.set(user, "ICy5YqxZB1uWSwcVLSNLcA==");

        System.out.println(user);
    }

    private void setAccessible(AccessibleObject object) {
        object.setAccessible(true);
    }
}

使用反射獲取泛型資訊

為了通過反射操作泛型以迎合實際開發的需要, Java新增了java.lang.reflect.ParameterizedType java.lang.reflect.GenericArrayTypejava.lang.reflect.TypeVariable java.lang.reflect.WildcardType幾種型別來代表不能歸一到Class型別但是又和原始型別同樣重要的型別.

型別 含義
ParameterizedType 一種引數化型別, 比如Collection<String>
GenericArrayType 一種元素型別是引數化型別或者型別變數的陣列型別
TypeVariable 各種型別變數的公共介面
WildcardType 一種萬用字元型別表示式, 如? ? extends Number ? super Integer

其中, 我們可以使用ParameterizedType來獲取泛型資訊.

public class Client {

    private Map<String, Object> objectMap;

    public void test(Map<String, User> map, String string) {
    }

    public Map<User, Bean> test() {
        return null;
    }

    /**
     * 測試屬性型別
     *
     * @throws NoSuchFieldException
     */
    @Test
    public void testFieldType() throws NoSuchFieldException {
        Field field = Client.class.getDeclaredField("objectMap");
        Type gType = field.getGenericType();
        // 列印type與generic type的區別
        System.out.println(field.getType());
        System.out.println(gType);
        System.out.println("**************");
        if (gType instanceof ParameterizedType) {
            ParameterizedType pType = (ParameterizedType) gType;
            Type[] types = pType.getActualTypeArguments();
            for (Type type : types) {
                System.out.println(type.toString());
            }
        }
    }

    /**
     * 測試引數型別
     *
     * @throws NoSuchMethodException
     */
    @Test
    public void testParamType() throws NoSuchMethodException {
        Method testMethod = Client.class.getMethod("test", Map.class, String.class);
        Type[] parameterTypes = testMethod.getGenericParameterTypes();
        for (Type type : parameterTypes) {
            System.out.println("type -> " + type);
            if (type instanceof ParameterizedType) {
                Type[] actualTypes = ((ParameterizedType) type).getActualTypeArguments();
                for (Type actualType : actualTypes) {
                    System.out.println("/tactual type -> " + actualType);
                }
            }
        }
    }

    /**
     * 測試返回值型別
     *
     * @throws NoSuchMethodException
     */
    @Test
    public void testReturnType() throws NoSuchMethodException {
        Method testMethod = Client.class.getMethod("test");
        Type returnType = testMethod.getGenericReturnType();
        System.out.println("return type -> " + returnType);

        if (returnType instanceof ParameterizedType) {
            Type[] actualTypes = ((ParameterizedType) returnType).getActualTypeArguments();
            for (Type actualType : actualTypes) {
                System.out.println("/tactual type -> " + actualType);
            }
        }
    }
}

使用反射獲取註解

使用反射獲取註解資訊的相關介紹, 請參看我的部落格Java註解實踐

反射效能測試

Method/Constructor/Field/Element都繼承了AccessibleObject,AccessibleObject類中有一個setAccessible方法:

public void setAccessible(boolean flag) throws SecurityException {
    ...
}

該方法有兩個作用:

1. 啟用/禁用訪問安全檢查開關:值為true,則指示反射的物件在使用時取消Java語言訪問檢查;值為false,則指示應該實施Java語言的訪問檢查;
2. 可以禁止安全檢查, 提高反射的執行效率.

/**
 * @author jifang
 * @since 15/12/31下午4:53.
 */
public class TestReflect {

    @Before
    public void testNoneReflect() {
        User user = new User();

        long start = System.currentTimeMillis();
        for (long i = 0; i < Integer.MAX_VALUE; ++i) {
            user.getName();
        }
        long count = System.currentTimeMillis() - start;
        System.out.println("沒有反射, 共消耗 <" + count + "> 毫秒");
    }

    @Test
    public void testNotAccess() throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
        User user = new User();
        Method method = Class.forName("com.fq.domain.User").getMethod("getName");

        long start = System.currentTimeMillis();
        for (long i = 0; i < Integer.MAX_VALUE; ++i) {
            method.invoke(user, null);
        }
        long count = System.currentTimeMillis() - start;
        System.out.println("沒有訪問許可權, 共消耗 <" + count + "> 毫秒");
    }

    @After
    public void testUseAccess() throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
        User user = new User();
        Method method = Class.forName("com.fq.domain.User").getMethod("getName");
        method.setAccessible(true);

        long start = System.currentTimeMillis();
        for (long i = 0; i < Integer.MAX_VALUE; ++i) {
            method.invoke(user, null);
        }
        long count = System.currentTimeMillis() - start;
        System.out.println("有訪問許可權, 共消耗 <" + count + "> 毫秒");
    }
}

執行上面程式,在我的機器上會有如下結果:

Java 反射

機器配置資訊如下:

Java 反射

可以看到使用反射會比直接呼叫慢3000毫秒,但是前提是該方法會執行20E+次(而且伺服器的效能也肯定比我的機器要高),因此在我們的實際開發中,其實是不用擔心反射機制帶來的效能消耗的,而且禁用訪問許可權檢查,也會有效能的提升。

相關文章