Java反射--实战篇

"talk is cheap, show me the code"。接下来将从实际应用的角度,说明如何使用反射。关于反射相关的概念,可以参考Java反射概述博文。 使用反射的基本步骤如下:
(1) 获取类型的Class对象;
(2) 基于Class对象获取类型元数据:Constructor类型、Field类型、Method类型;
(3) 基于Constructor类型、Field类型、Method类型访问成员。

获取Class对象

Class类是由class关键字修饰的一个特殊的类。Class实例可以看成是Object及其子类的元数据引用。一个Java类均对应一个Class实例。该Class实例引用可以从getClass方法获取。使用Class对象可以获取对应类型的元数据(metadata)信息,如构造器、字段、方法等。
获取Class对象的方法有很多种。这里介绍下常用的几种方法:
(1) 使用Class类的forName静态方法;
(2) 使用Ojbect根类的class静态字段;
(3) 使用类型实例的getClass()方法;
(4) 使用ClassLoader实例的loadClass方法。
在介绍获取Class对象的方法前,先预定义测试类,方便后面统一使用。

// package io.github.courage007.reflect;

@Getter
@Setter
public class Apple {
    
    
    private String color;
    
    private String size;

    public Apple() {
    
    
        this.color = "red";
        this.size = "medium";
    }

    public Apple(String color, String size) {
    
    
        this.color = color;
        this.size = size;
    }

    public void changeByTime() {
    
    
        this.color = "gray";
        this.size = "small";
    }

    private void changeColor() {
    
    
        this.color = "gray";
    }
}

使用Class类的forName静态方法

Class类提供forName静态方法用于获取Class实例。相关源码片段如下:

public final class Class<T> implements java.io.Serializable,
                              GenericDeclaration,
                              Type,
                              AnnotatedElement {
    
    
    // ...
    @CallerSensitive
    public static Class<?> forName(String className)
                throws ClassNotFoundException {
    
    
        Class<?> caller = Reflection.getCallerClass();
        return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
    }

    @CallerSensitive
    public static Class<?> forName(String name, boolean initialize,
                                   ClassLoader loader)
        throws ClassNotFoundException
    {
    
    
        Class<?> caller = null;
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
    
    
            // Reflective call to get caller class is only needed if a security manager
            // is present.  Avoid the overhead of making this call otherwise.
            caller = Reflection.getCallerClass();
            if (loader == null) {
    
    
                ClassLoader ccl = ClassLoader.getClassLoader(caller);
                if (ccl != null) {
    
    
                    sm.checkPermission(
                        SecurityConstants.GET_CLASSLOADER_PERMISSION);
                }
            }
        }
        return forName0(name, initialize, loader, caller);
    }

    // since java 9
    @CallerSensitive
    public static Class<?> forName(Module module, String name) {
    
    
        Objects.requireNonNull(module);
        Objects.requireNonNull(name);

        ClassLoader cl;
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
    
    
            Class<?> caller = Reflection.getCallerClass();
            if (caller != null && caller.getModule() != module) {
    
    
                // if caller is null, Class.forName is the last java frame on the stack.
                // java.base has all permissions
                sm.checkPermission(SecurityConstants.GET_CLASSLOADER_PERMISSION);
            }
            PrivilegedAction<ClassLoader> pa = module::getClassLoader;
            cl = AccessController.doPrivileged(pa);
        } else {
    
    
            cl = module.getClassLoader();
        }

        if (cl != null) {
    
    
            return cl.loadClass(module, name);
        } else {
    
    
            return BootLoader.loadClass(module, name);
        }
    }

    /** Called after security check for system loader access checks have been made. */
    private static native Class<?> forName0(String name, boolean initialize,
                                            ClassLoader loader,
                                            Class<?> caller)
        throws ClassNotFoundException;
}

可以看到,forName静态方法有三个重载方法。其使用示例如下:

public void getClassRefByForName() {
    
    
    try {
    
    
        Class clazz1 = Class.forName("io.github.courage007.reflect.Apple");
        // Returns the class of the caller of the method calling this method
        // 获取调用当前方法的方法的调用者
        Class<?> caller = Reflection.getCallerClass();
        Class clazz = Class.forName("io.github.courage007.reflect.Apple", false, caller.getClassLoader());
    } catch (ClassNotFoundException ex) {
    
    
        throw new RuntimeException("get class failed");
    }
    // java 9新增基于Module获取Class实例,这里不再讨论,有兴趣的同学可以自行学习
}

基于类的全路径名获取 Class 对象的方式,其优势是不需要事先引入依赖,适用于运行时调用的场景。需要说明的是,由于代码里硬编码了类的全路径名,不能很好的适应类的全路径名变化场景,生产上可以这部分数据放置在文件或数据库等存储介质中。

使用Ojbect根类的静态class字段

Ojbect根类提供静态class字段,可以直接获取Class实例。示例代码如下:

public void getClassRefByStaticField() {
    
    
    Class clazz = Apple.class;
}

使用这种方法,需要引入类对应的包。该方式主要应用于调用方。

使用类型实例的getClass()方法

Ojbect根类提供getClass方法,用于获取实例的Class实例。关键代码如下:

public class Object {
    
    
    // ...

    /**
     * Returns the runtime class of this {@code Object}. The returned
     * {@code Class} object is the object that is locked by {@code
     * static synchronized} methods of the represented class.
     */
    @HotSpotIntrinsicCandidate
    public final native Class<?> getClass();
}

所以,可以使用getClass方法获取Class实例。示例代码如下:

public void getClassRefByGetClassMethod() {
    
    
    Apple apple = new Apple();
    Class clazz = apple.getClass();
}

需要说明的是,使用这种方法,需要引入实例所属类的包。这种方式的调用多出现于调用方。对于自身来说,因为已经有了类的实例,无需再通过Class实例去构造实例并访问字段或方法。

使用ClassLoader实例的loadClass方法

ClassLoader实例提供loadClass方法来实现指定类的全路径名来获取Class实例。示例代码如下:

public void getClassRefByClassLoader() {
    
    
    try {
    
    
        ClassLoader classLoader = this.getClass().getClassLoader();
        Class clazz = classLoader.loadClass("io.github.courage007.reflect.Apple");
        System.out.println(clazz.toString());
    } catch (ClassNotFoundException ex) {
    
    
        throw new RuntimeException("get class failed");
    }
}

基于ClassLoader获取Class的方式与基于Class类的forName静态方法获取Class的方式一样,都是基于类的全路径名获取 Class 对象。其优缺点不再赘述。

基于反射构造实例

获取Class对象后,就可基于Class对象实现构造方法、字段、方法的调用。这里介绍如何基于Class对象调用构造方法以实现实例创建。
按照访问类型、参数个数,可将构造函数分为如下四类:公有无参构造函数公有带参构造函数私有无参构造函数私有带参构造函数

公有无参构造函数

Class类提供newInstance方法,用于创建类的实例。但是,该方法会返回空实例,在Java 9之后已经弃用,推荐先基于Class获取Constructor实例,然后基于Constructor的newInstance方法去创建类型实例。需要说明的是Constructor的newInstance方法也可创建公有带参构造函数,示例代码如下:

public void getInstanceWithoutParam() {
    
    
    try {
    
    
        Class clazz = Class.forName("io.github.courage007.reflect.Apple");
        Apple apple = (Apple) clazz.getConstructor(null).newInstance(null);
        apple.getColor();
    } catch (ClassNotFoundException | NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException ex) {
    
    
        throw new RuntimeException("get class constructor failed");
    }
}

已弃用的Class类的newInstance方法代码片段如下:

public final class Class<T> implements java.io.Serializable,
                              GenericDeclaration,
                              Type,
                              AnnotatedElement {
    
    
    // ...
    /**
     * 创建基于Class实例的类的实例。
     *
     * @deprecated 这个方法会传递空实例,已被弃用。推荐使用
     * java.lang.reflect.Constructor的newInstance(java.lang.Object...)方法
     */
    @CallerSensitive
    @Deprecated(since="9")
    public T newInstance() throws InstantiationException, IllegalAccessException {
    
    
        // ...
        try {
    
    
            // 创建无参
            return tmpConstructor.newInstance((Object[])null);
        } catch (InvocationTargetException e) {
    
    
            Unsafe.getUnsafe().throwException(e.getTargetException());
            // Not reached
            return null;
        }
    }
}

公有带参构造函数

Constructor的newInstance方法支持创建公有带参构造函数,示例代码如下:

public void getInstanceWithParam() {
    
    
    try {
    
    
        Class clazz = Class.forName("io.github.courage007.reflect.Apple");
        Apple apple = (Apple) clazz.getConstructor(String.class, String.class).newInstance("green", "small");
        apple.getColor();
    } catch (ClassNotFoundException | NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException ex) {
    
    
        throw new RuntimeException("get class constructor failed");
    }
}

私有构造函数

Class实例的getConstructor方法只能获取包含父类的某个public的构造方法,对于某个非public访问权限的构造方法,则需使用
getDeclaredConstructor方法。需要说明的是,在使用非public的Constructor时,必须先执行setAccessible(true)方法,设置允许访问。
经过对公有构造函数调用可以发现,基于Constructor调用公有无参构造函数和公有待参构造函数,其差异性仅体现在传参。私有无参构造函数和私有带参构造函数有类似处理机制。这里仅以私有带参构造函数为例,私有无参构造函数处理类似。示例代码如下:

public void getInstanceWithPrivateConstructor() {
    
    
    try {
    
    
        Class clazz = Class.forName("io.github.courage007.reflect.Apple");
        Constructor currentConstructor = clazz.getDeclaredConstructor(String.class);
        currentConstructor.setAccessible(true);
        Apple apple = (Apple)currentConstructor.newInstance("yellow");
        apple.getColor();
    } catch (ClassNotFoundException | NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException ex) {
    
    
        throw new RuntimeException("get class constructor failed");
    }
}

基于反射获取字段

与基于反射构造实例先通过Class对象获取Constructor对象,然后再基于Constructor对象调用构造函数类似,基于反射获取字段先通过Class对象获取Field对象,然后再基于Field对象访问字段。Field提供一系列方法,用于操作字段,常用的方法有:

getName():返回字段名称  
getType():返回字段类型,也是一个Class实例,如String.class  
getModifiers():返回字段的修饰符,它是一个int,不同的bit表示不同的访问权限  
setAccessible():设置变量为public  
set():设置字段值  
get():获取字段值  

这里以访问私有字段为例,介绍下如何基于反射访问字段。示例代码如下:

public void getFieldWithPrivatePermission() {
    
    
    try {
    
    
        Class clazz = Class.forName("io.github.courage007.reflect.Apple");
        // 获取private字段"grade":
        Field colorField = clazz.getDeclaredField("color");
        colorField.setAccessible(true);
        // 获取字段名
        colorField.getName();
        // 获取字段类型
        colorField.getType();
        // 获取字段的访问权限
        colorField.getModifiers();
        Apple apple = (Apple) clazz.getConstructor(String.class, String.class).newInstance("green", "small");
        apple.getColor();
        // 设置字段
        colorField.set(apple, "red");
        // 获取字段值
        colorField.get(apple);
    } catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException |  InstantiationException | InvocationTargetException | NoSuchMethodException ex) {
    
    
        throw new RuntimeException("get class field failed");
    }
}

基于反射获取方法

与基于反射构造实例和基于反射获取字段类似,基于反射获取方法先通过Class对象获取Method对象,然后再基于Method对象访问方法。Method提供一系列方法,用于访问方法,常用的方法有:

getName():返回方法名称  
getReturnType():返回方法返回值类型,也是一个Class实例,例如:String.class  
getParameterTypes():返回方法的参数类型,是一个Class数组,例如:{String.class, int.class}  
getModifiers():返回方法的修饰符,它是一个int,不同的bit表示不同的含义  
setAccessible():设置private函数为public属性  
invoke(object,new Object[]{}) 调用执行方法  

这里以访问私有方法为例,介绍下如何基于反射调用方法。示例代码如下:

public void invokeMethodWithPrivatePermission() {
    
    
    try {
    
    
        Class clazz = Class.forName("io.github.courage007.reflect.Apple");
        Method changeColorMethod = clazz.getDeclaredMethod("changeColor");
        changeColorMethod.setAccessible(true);
        // 获取方法名
        changeColorMethod.getName();
        // 获取参数类型 Class[] 数组
        changeColorMethod.getParameterTypes();
        // 返回方法返回值类型 Class 实例
        changeColorMethod.getReturnType();
        // 获取方法的访问权限
        changeColorMethod.getModifiers();
        Apple apple = (Apple) clazz.getConstructor(String.class, String.class).newInstance("green", "small");
        // 调用方法
        changeColorMethod.invoke(apple, null);
    } catch (ClassNotFoundException | IllegalAccessException |  InstantiationException | InvocationTargetException | NoSuchMethodException ex) {
    
    
        throw new RuntimeException("get class method failed");
    }
}

基于反射获取父类成员

通过Class实例的getXxx类型方法可以获取包含父类的某个public的构造方法、字段、方法。对于非public构造方法、字段、方法,可以先通过获取getSuperclass获取父类对应的Class对象,然后通过Class对象访问非public构造方法、字段、方法。这里不再给出示例,有兴趣的同学,可以自行学习。

总结

Java提供Class类型、Constructor类型、Field类型、Method类型,帮助实现运行时访问类型实例上的成员。在获取成员时,根据成员的访问权限、声明位置,需要选用不同的方法,具体可以分为两类:
getXxx 获取包含父类的某个public的构造方法、字段、方法。
getDeclaredXxx 获取当前类的包含private访问权限的所有构造方法、字段、方法。

参考

https://www.anquanke.com/post/id/245458 Java安全之反射

猜你喜欢

转载自blog.csdn.net/wangxufa/article/details/123298638