Java反射机制

一、介绍

Java反射是指在运行时检查或操作类、接口、字段或方法的能力。通过使用反射,可以在运行时获取类的信息、构造对象、执行方法、访问或修改字段等,而不需要事先知道类的结构。在Java中,反射功能由java.lang.reflect包提供支持。通过使用java.lang.reflect类中的Class、Constructor、Method、Field等类,可以实现对类的各种操作。使用反射可以实现一些高级的动态操作,比如动态代码生成、插件系统、对象序列化等。

public class Student {

    public String name;

    private int age;
    
    final boolean isGraduation = Boolean.FALSE;

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    private void study() {
        System.out.println("学生正在学习!!!");
    }

    public void setName(String name) {
        System.out.println("setName: " + name);
        this.name = name;
    }

    public void setAge(int age) {
        System.out.println("setAge: " + age);
        this.age = age;
    }

    @Override
    public String toString() {
        return "Student " + name + " is " + age + "-years old";
    }
}

【正射】
我们在编写代码时,当需要使用到某一个类的时候,都会先了解这个类是做什么的,然后实例化这个类,接着用实例化好的对象进行操作,这就是正射。

Student s1 = new Student("小明", 12);

打印:Student 小明 is 12-years old

【反射】
反射就是,一开始并不知道我们要初始化的类对象是什么,自然也无法使用 new 关键字来创建对象了。这就需要借助反射工具(api)来实例化我们需要访问的类。JDK从1.1开始就支持反射,使用java.lang.reflect包里的工具。

Class<?> studentClass = Class.forName("reflectiontest.Student");
Constructor<?> studentClassConstructor = studentClass.getConstructor(String.class, int.class);
Object s2 = studentClassConstructor.newInstance("小红", 10);

打印:Studentg 小红 is 10-years old

所以,这两段代码的区别就在于,正射是在运行前就已经知道要运行的类是Student;反射是在程序运行的时候才通过字符串"reflectiontest.Student"知道操作的类。

Class类

一个类中有属性,方法,构造器等,每个类可能都不一样,所以需要一个类,用来描述类,这就是Class。Class是用来描述类的类,它封装了当前对象所对应的类的信息。Java通过以下两种方式在运行时识别对象的类型和类的信息:

  • RTTI(Run-Time Type Identification)运行时类型识别
  • 反射机制

Class类是一个对象照镜子的结果,对象可以看到自己有哪些属性,方法,构造器,实现了哪些接口等等,对于每个类而言,JRE 都为其保留一个不变的 Class 类型的对象。一个类(而不是一个对象)在 JVM 中只会有一个Class实例。



获取Class对象的四种方式:

  1. 类的.class方法
Class<Student> clazz = Student.class;
  1. 使用Class.forName()方法
Class<?> clazz= Class.forName("reflectiontest.Student");
  1. 使用实例对象的 getClass() 方法
Student s1 = new Student("小明", 12);
Class<Student> clazz = s1.getClass();
  1. 根据类加载器获取
URL url = new URL("file:XXX\\Student.java");
ClassLoader classLoader = new URLClassLoader(new URL[]{url});
Class<Student> clazz= classLoader.loadClass("reflectiontest.Student");

下面列举Class类常用方法和说明,

方法 说明
static Class forName(String className) 返回指定类名name的Class 对象
Object newInstance() 调用缺省构造函数,返回该Class对象的一个实例(已过时)
String getName() 返回该Class对象所表示的实体(类、接口、数组类、基本类型或void)名称
Class getSuperclass() 返回当前Class对象的父类Class对象
String getPackageName() 返回当前Class对象所在的包名
Class[] getInterfaces() 返回当前Class对象实现的接口
ClassLoader getClassLoader() 返回该类的类加载器
Constructor[] getConstructors() 返回该类的所有公开构造方法
Constructor[] getDeclaredConstructors() 返回该类的所有构造方法
Method[] getMethods() 返回该类的所有公开方法,包括继承的公开方法
Method[] getDeclaredMethods() 返回该类的所有方法,不包括继承的方法
Field[] getFields() 返回该类的所有公开属性,包括继成的公开属性
Field[] getDeclaredFields() 返回该类的所有属性,不包括集成的属性

上面方法中的Constructor、Method、Field均来自java.lang.reflect包。

Class对象是反射机制的核心,只有获取到一个类的Class对象,才能通过Class对象包含的类信息还原最初的类,即反射的核心要义。下面列举几个常见的例子。

  1. 反射实例化对象
// Class.forName反射获取class
Class<?> studentClass = Class.forName("reflectiontest.Student");
// 获取指定参数类型的构造方法
Constructor<?> constructor = studentClass.getConstructor(String.class, int.class);
// 通过newInstance实例化class
Object student = constructor.newInstance("小红", 10);
  1. 反射获取私有方法
Method study = studentClass.getDeclaredMethod("study");
// 私有方法需要设置accessible=true以开放其访问权限
study.setAccessible(true);
// 调用study方法
study.invoke(student);
  1. 反射获取私有属性
Field age = studentClass.getDeclaredField("age");
// 私有属性设置accessible=true以开放其访问权限
age.setAccessible(true);
// 获取student实例的age值
int intAge = (int) age.get(student);
// 设置实例student的age=9
age.set(student, 9);
  1. 修改final修饰的属性值(危险操作)
Field isGraduation = studentClass.getDeclaredField("isGraduation");
isGraduation.setAccessible(true);
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
// 把isGraduation中的final修饰符去掉
modifiersField.setInt(isGraduation, isGraduation.getModifiers() & ~Modifier.FINAL);
// 修改student中isGraduation=true
isGraduation.setBoolean(student, true);

注意,只有非内联优化的变量才能通过反射修改值,内联优化的final属性是无法被修改的。这种做法是不推荐的,因为它会破坏final属性的设计初衷,可能会导致不可预测的行为和安全问题。在实际开发中,应该遵循final属性的设计原则,不要试图绕过它的限制。

二、原理

参考 https://www.cnblogs.com/yougewe/p/10125073.html

三、应用

1. Android LayoutInlater

我们在使用xml进行界面布局的时候,layout.xml里的每个节点就是通过反射转换成对应View的Java类的。在Activity的onCreate生命周期函数中调用setContentView(layoutResId)来绑定布局,最终会调到LayoutInflater.inflate(layoutResId)中,

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
    ...
    
    final View temp = createViewFromTag(root, name, inflaterContext, attrs);
    
    ...
}

View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
        boolean ignoreThemeAttr) {
    ...
    
    view = createView(context, name, null, attrs);

    ...
}

public final View createView(@NonNull Context viewContext, @NonNull String name,
        @Nullable String prefix, @Nullable AttributeSet attrs)
        throws ClassNotFoundException, InflateException {
    ...
    
    // 缓存中查找View的构造方法
    Constructor<? extends View> constructor = sConstructorMap.get(name);
    Class<? extends View> clazz = null;

    if (constructor == null) {
        // 反射获取View的Class对象
        clazz = Class.forName(prefix != null ? (prefix + name) : name, false,
                mContext.getClassLoader()).asSubclass(View.class);

        // 获取构造方法
        constructor = clazz.getConstructor(mConstructorSignature);
        constructor.setAccessible(true);
        sConstructorMap.put(name, constructor);
    } else {
        ...
    }

    // 实例化View
    final View view = constructor.newInstance(args);          
    return view;
        
    ...
}

2. 动态代理

动态代理是通过反射机制动态生成代理者对象的一种设计模式,区别于静态代理,动态代理在运行时按需动态生成目标代理对象,解决了静态代理模式服务对象单一、扩展性低的问题。JDK提供了实现动态代理的顶层类java.util.reflect.Proxy,可以通过Proxy类的静态方法Proxy.newProxyInstance()动态创建一个类的代理类,并实例化。由它创建的代理类都是Proxy类的子类。

/**
 * 定义目标对象的抽象接口
 */
public interface ISubject {

    void buy();
}

/**
 * 实现目标类(实现 ISubject 接口)
 */
public class Buyer implements ISubject {
    @Override
    public void buy() {
        System.out.println("Buyer buy");
    }
}

/**
 * 声明调用处理类(实现 InvocationHandler 接口)
 */
public class SubjectInvocationHandler implements InvocationHandler {

    private final ISubject mTarget;

    public SubjectInvocationHandler(ISubject target) {
        this.mTarget = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("proxy invoke, proxy = " + proxy.getClass() + ", mTarget = " + mTarget.getClass());
        return method.invoke(mTarget, args);
    }
}

public class DynamicProxyTest {

    public static void main(String[] args) {
        Buyer buyer = new Buyer();
        SubjectInvocationHandler handler = new SubjectInvocationHandler(buyer);
        
        ISubject buyerProxy = (ISubject) Proxy.newProxyInstance(
            buyer.getClass().getClassLoader(),
            buyer.getClass().getInterfaces(),
            handler);
            
        buyerProxy.buy();
    }
}

打印:
proxy invoke, proxy = class com.sun.proxy.$Proxy0, mTarget = class reflectiontest.dynamicproxy.realobj.Buyer
Buyer buy

打印结果显示生成了中间com.sun.proxy.$Proxy0,我们通过在main方法中加下面这段代码,保留生成的代码。变量为 true 时,将会在工程目录下生成 $Proxy0 的 class 文件。

// JDK 1.8以前
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");

// JDK 1.8以后
System.getProperties().put("jdk.proxy.ProxyGenerator.saveGeneratedFiles", "true");
// 或
System.setProperty("jdk.proxy.ProxyGenerator.saveGeneratedFiles", "true");
package com.sun.proxy;

public final class $Proxy0 extends Proxy implements ISubject {
    ...
    
    private static Method m3;

    public $Proxy0(InvocationHandler var1) throws  {
        super(var1);
    }

    ...

    public final void buy() throws  {
        try {
            // h是父类Proxy中的InvocationHandler类型成员变量
            // 通过代理类访问目标对象的方法
            // 最终会通过 super.h.invoke() 回调到我们重写的 InvocationHandler 实现类的 invoke() 中
            super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    static {
        try {
            ...
            
            // 通过反射获取目标类实现方法
            m3 = Class.forName("reflectiontest.dynamicproxy.ISubject").getMethod("buy");
            
            ...
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

下面简单看看这个$Proxy0是怎么生成的,

public static Object newProxyInstance(ClassLoader loader,
                                      Class<?>[] interfaces,
                                      InvocationHandler h) {
    Objects.requireNonNull(h);

    final Class<?> caller = System.getSecurityManager() == null
                                ? null
                                : Reflection.getCallerClass();

    /*
     * Look up or generate the designated proxy class and its constructor.
     */
    Constructor<?> cons = getProxyConstructor(caller, loader, interfaces);

    return newProxyInstance(caller, cons, h);
}

private static Object newProxyInstance(Class<?> caller, // null if no SecurityManager
                                       Constructor<?> cons,
                                       InvocationHandler h) {
    /*
     * Invoke its constructor with the designated invocation handler.
     */
    ...

    // 通过构造器实例化代理对象   
    return cons.newInstance(new Object[]{h});
    
    ...
}

private static Constructor<?> getProxyConstructor(Class<?> caller,
                                                  ClassLoader loader,
                                                  Class<?>... interfaces) {
    // optimization for single interface
    if (interfaces.length == 1) {
        ...
        
        return proxyCache.sub(intf).computeIfAbsent(
            loader,
            (ld, clv) -> new ProxyBuilder(ld, clv.key()).build()
        );
    } else {
        ...
    }
}

/** parameter types of a proxy class constructor */
private static final Class<?>[] constructorParams =
    { InvocationHandler.class };

Constructor<?> build() {
    // 根据module和interfaces信息构造代理的Class对象,代理默认含有有个InvocationHandler参数类型的构造方法
    Class<?> proxyClass = defineProxyClass(module, interfaces);
    final Constructor<?> cons;
    
    ...
    
    cons = proxyClass.getConstructor(constructorParams);
    
    ...
    
    return cons;
}

3. 注解

四、思考

1. 反射解决了什么问题

  1. 动态加载类:Java反射允许在运行时动态加载类,而不是在编译时就确定需要使用的类。
  2. 运行时获取类信息:通过反射可以在运行时获得类的信息,如类名、方法名、字段名、注解等,这使得可以在运行时动态地操作类和对象。
  3. 动态调用方法:使用反射可以在运行时动态调用类的方法,这对于框架和库的设计和使用非常有用。
  4. 修改私有字段和方法:反射允许访问和修改类的私有字段和方法,这在某些情况下是很有用的,但也需要小心使用,以避免破坏类的封装性。

应用场景一:有的类是我们在编写程序的时候无法使用new一个对象来实例化对象的。例如:

  • 调用的是来自网络的二进制.class文件,而没有其.java代码
  • 注解。运行时注解包含的元数据只能通过反射获取

应用场景二:动态加载(可以最大限度的体现Java的灵活性,并降低类的耦合性:多态)。有的类可以在用到时再动态加载到jvm中,这样可以减少jvm的启动时间,同时更重要的是可以动态的加载需要的对象(多态)

  • 动态代理

应用场景三:避免将程序写死到代码里。比如Java通过new关键字创建了一个对象并编译成class字节码文件,如果我想替换这个对象,就得重新编译一个新的class文件,但如果使用反射Class.forName(String name)就可以从配置文件中读取要实例化的对象而无需重新编译。

  • Spring框架中通过xml配置JavaBean

2. 效率问题

下面举个例子,

public class DynamicProxyTest {

    public static long timeDiff(long old) {
        return System.currentTimeMillis() - old;
    }

    public static void main(String[] args) {
    
        long numTrials = (long) Math.pow(10, 7);
        long millis = System.currentTimeMillis();
        
        for (int i = 0; i < numTrials; i++) {
            new Student();
        }
        System.out.println("Normal instaniation took: "
                + timeDiff(millis) + "ms");
        
        millis = System.currentTimeMillis();
        
        Class<Student> c = Student.class;
        
        for (int i = 0; i < numTrials; i++) {
            c.getConstructor().newInstance();
        }
        
        System.out.println("Reflecting instantiation took:"
                + timeDiff(millis) + "ms");
    }
}

打印:
Normal instaniation took: 83ms
Reflecting instantiation took:510ms

Java反射机制性能低下有以下几个原因:

  1. 动态类型检查:在使用反射时,编译器无法对代码进行类型检查,而是在运行时才能确定访问对象的类型。这意味着在每次使用反射时都需要做类型检查和转换操作,增加了运行时的开销。比如对方法参数的装包和拆包。
  2. 方法调用开销:通过反射调用方法时,通常需要使用Method对象的invoke()方法,这会涉及方法查找、参数类型匹配等操作,相比直接调用方法会更耗时。
  3. 访问控制检查:反射机制允许改变类的私有字段或方法的访问权限,因此在访问私有成员时需要进行额外的访问控制检查,增加了开销。
  4. 缓存未命中:由于反射操作是动态的,不同于直接调用方法或访问字段,因此缓存机制可能失效,导致性能下降。
  5. 无法使用编译器优化:由于反射操作是动态的,所以无法使用某些编译器优化,比如内联优化,因为调用的具体方法在运行时才确定。

3. 安全问题

反射允许在运行时动态调用类的私有资源,这可能会引起意料之外的结果,造成程序功能失常。下面以单例为例,单例允许全局有且仅有一个实例,因此一般私有化其构造方法,但反射能通过修改构造方法权限实例化对象,造成单例失效。下面尝试一下单例模式的防反射攻击。

  • 饿汉模式
public class SingleClass {

    private final static SingleClass mInstance = new SingleClass();

    private SingleClass() {
        if (mInstance != null) {
            throw new RuntimeException("单例模式不允许重复创建!");
        }
    }

    public static SingleClass getInstance() {
        return mInstance;
    }
}

public static void main(String[] args) {
    Constructor<SingleClass> constructor = SingleClass.class.getDeclaredConstructor();
    constructor.setAccessible(true);
    constructor.newInstance();
}

异常:


  • 懒汉模式
public class SingleClass {

    private volatile  static SingleClass mInstance;

    private SingleClass() {
        if (mInstance != null) {
            throw new RuntimeException("单例模式不允许重复创建!");
        }
    }

    public static SingleClass getInstance() {
        if (mInstance == null) {
            synchronized (SingleClass.class) {
                if (mInstance == null) {
                    mInstance = new SingleClass();
                }
            }
        }
        return mInstance;
    }
}

public static void main(String[] args) {
    SingleClass singleClass = SingleClass.getInstance();
    log(singleClass.toString());

    Constructor<SingleClass> constructor = SingleClass.class.getDeclaredConstructor();
    constructor.setAccessible(true);
    SingeClass reflectionSingeClass = (SingeClass) constructor.newInstance();
    log(reflectionSingeClass.toString());
}

但是如果将getInstance和反射初始化调整顺序,单例就会被破坏。



如果在单例中增加标志位flag,如下,

private SingleClass() {
    if (flag) {
        throw new RuntimeException("单例模式不允许重复创建!");
    } else {
        flag = true;
    }
}

这样也是不行的,因为变量flag值可以通过反射修改。
综上,这种局限于构造顺序的防反射攻击方法仅对饿汉模式有作用,对于懒汉模式是没有作用的。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容