就非得用反射才行吗?

乐活   2024-10-14 15:36   北京  


我的个人博客:www.moonkite.cn

大家好,我是风筝

不是说反射会影响性能吗,那为什么 Java 中有那么多地方还用反射,不用不行吗?

答案是:不行,除非 Java 提供了更好的方法。

什么是反射

不用多说,反射是 Java 学习中逃不掉的概念,属于进阶一点的知识点。

有人说,我不会反射,而且平时也根本不用反射呀。没错,大多数情况下,你不主动用它也能完成绝大多数开发任务。

这其中有一个很重要的隐藏信息,就是现在用的框架都封装的非常好,让你感觉不到你在用反射,而实际框架的内部,有很多地方要依靠反射的能力。

好了,先用简单的几点概括一下反射是什么:

  1. 运行时自省:允许程序在运行时检查自身的结构和状态。
  2. 动态访问:能够在运行时访问类、方法、字段等,而不需要在编译时知道它们的名称。
  3. 类型操作:提供了在运行时操作类型信息的能力,包括创建实例、调用方法、修改字段值等。
  4. 打破封装:允许访问和修改私有成员,从而在必要时突破常规的访问限制。
  5. 元数据处理:支持在运行时读取和处理注解等元数据信息。
  6. 动态加载:能够在运行时动态加载和使用类。
  7. 灵活性增强:为程序提供了极大的灵活性,使得可以编写更通用、可扩展的代码。

反射的用法

Java 反射主要涉及以下核心类,它们都位于 java.lang.reflect 包中,具体用法不用多说,复制粘贴,说来就来。

这里举一个最简单的例子:

// 获取类的 Class 对象
Class<?> clazz = MyClass.class;

// 创建实例
Object obj = clazz.newInstance();

// 获取方法
Method method = clazz.getMethod("myMethod", String.class);

// 调用方法
method.invoke(obj, "Hello, Reflection!");

// 获取字段
Field field = clazz.getDeclaredField("myField");

// 设置字段可访问(如果是私有的)
field.setAccessible(true);

// 设置字段值
field.set(obj, "New Value");

反射的作用

反射的作用全都围绕运行时展开,我们知道,一个程序主要包括编译期和运行时两个大阶段。

编译期会检查语言能确定的部分,比如语法是否正确,以及事先已知的操作,比如要访问一个 User对象的 name属性。

而到了运行时,代码已经固定了,这时候再想对已有程序做一些修改和扩展,最常见的方法就是改代码,然后重新编译,重新运行。

而很多情况下,这是不可预见的。举个最经典的例子,就是数据库驱动的例子,尤其是我们用 Spring Boot或Spring 开发,都会把数据库配置放到配置文件中。在程序开始运行之前,应用程序是不知道到底配置了哪种数据库的,是 MySQL 还是 PostgreSQL,都是未知数,在这种情况下,在运行时动态获取当前配置的数据库信息就变得非常关键了。

String driverClassName = getConfig("db.driverClassName"); // 可能是 "com.mysql.cj.jdbc.Driver" 或其他驱动类
Class.forName(driverClassName);

好了,那反射具体能干什么呢?

运行时类型检查:可以在运行时确定对象的类型。动态加载类:可以在运行时加载、使用编译期间完全未知的类。获取类信息:可以在运行时获取类的方法、字段、构造函数等信息。创建对象实例:可以动态创建对象实例,而无需通过 new 关键字。调用方法:可以在运行时调用任意方法,包括私有方法。操作字段:可以在运行时获取或设置对象的任意字段值,包括私有字段。操作数组:可以动态创建数组,获取数组长度,读写数组元素等。

概括起来,就是能够利用反射完成对对象的操作,只不过是在运行时完成的。

反射的应用场景

那反射到底用在什么场景下呢?

说句正确的废话,当你碰到需要用到合适的场景时,自然而然就会发现反射。

我上一次主动用反射是这样的一个场景:有一个字段非常多的内容,也就一百多个吧,还好直接导入的,不用在页面上一个个填,但是展示的时候要把这一百多个字段一一展示出来。

这样的场景下,想想能用什么办法呢?

当然了,按照常规展示表单的方法,定义一个包含这一百多个属性的实体类,然后一一赋值,最后返给前端。这也不是不可以,但是这么多字段,够前后端喝一壶的了。那有没有更好的方法呢?

当然有了,反射就是不错的选择,有了反射,只需要遍历所有属性,然后对照字典表,把前端要展示的字段显示名和值返回一个列表给前端就好了,前端更省事儿了,直接循环列表。代码量和开发时间指数下降。

这个算是比较特殊的例子了,但是即便不是这样,很多 Bean 拷贝的库也都是用反射实现的。

接下来看一看反射主要的使用场景,而且是我们经常用到的。

Spring 依赖注入

依赖注入是每个 Java 开发者都绕不过去的面试题,是 Spring 框架的核心技术。

Spring 中大量使用反射来实现其依赖注入机制。

我们在使用 Spring 时,肯定在开发过程中碰到过类似于下面的这种异常,从中可以看出是进行反射调用的时候发生了异常。

java.lang.NullPointerException 
sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:87)
  at java.lang.reflect.Method.invoke(Method.java:292)

下面用几个简单的例子演示Spring 中是如何注入被 @Autowired 注解标识的服务类的。

public class TestContainer {
    private Map<Class<?>, Object> container = new HashMap<>();

    public void register(Class<?> clazz) throws Exception {
        Constructor<?> constructor = clazz.getDeclaredConstructor();
        Object instance = constructor.newInstance();
        
        for (Field field : clazz.getDeclaredFields()) {
            if (field.isAnnotationPresent(Autowired.class)) {
                Class<?> dependencyClass = field.getType();
                Object dependency = container.get(dependencyClass);
                
                field.setAccessible(true);
                field.set(instance, dependency);
            }
        }
        
        container.put(clazz, instance);
    }

    public <T> get(Class<T> clazz) {
        return clazz.cast(container.get(clazz));
    }
}

在上述代码中:

  1. register 方法使用反射来创建类的实例。
  2. 它遍历类的所有字段,检查是否有 @Autowired 注解。
  3. 对于有注解的字段,使用反射来设置字段的值。

ORM 框架

ORM 框架更是把反射用到了极致,写ORM框架的人肯定是不知道将来你的数据库中有多少个表,每个表有多少字段,字段的名字都是什么。但是最终需要将数据表里的字段值映射到 Java 对象来。

下面是一个简单的示例:

public class SimpleORM {
    public <T> mapResultSetToObject(ResultSet rs, Class<T> clazz) throws Exception {
        T instance = clazz.getDeclaredConstructor().newInstance();
        
        for (Field field : clazz.getDeclaredFields()) {
            if (field.isAnnotationPresent(Column.class)) {
                String columnName = field.getAnnotation(Column.class).name();
                Object value = rs.getObject(columnName);
                
                field.setAccessible(true);
                field.set(instance, value);
            }
        }
        
        return instance;
    }
}

@Retention(RetentionPolicy.RUNTIME)
@interface Column {
    String name();
}

class User {
    @Column(name = "id")
    private int id;
    
    @Column(name = "name")
    private String name;

    // getters and setters
}
  1. mapResultSetToObject 方法使用反射创建类的实例。
  2. 它遍历类的所有字段,检查是否有 @Column 注解。
  3. 对于有注解的字段,它使用反射来设置字段的值,值来自 ResultSet

单元测试中

JUnit 等测试框架使用反射来发现和运行测试方法。我们用 JUnit 写单元测试用例的时候,会在测试方法上加上 @Test的注解。然后项目编译的时候,会运行测试用例,测试框架是如何找到测试用例方法的呢,就是通过反射。

public class SimpleTestRunner {
    public void runTests(Class<?> testClass) throws Exception {
        Object testInstance = testClass.getDeclaredConstructor().newInstance();
        
        for (Method method : testClass.getDeclaredMethods()) {
            if (method.isAnnotationPresent(Test.class)) {
                try {
                    method.invoke(testInstance);
                    System.out.println(method.getName() + " passed");
                } catch (InvocationTargetException e) {
                    System.out.println(method.getName() + " failed: " + e.getCause().getMessage());
                }
            }
        }
    }
}

@Retention(RetentionPolicy.RUNTIME)
@interface Test {}

class MyTests {
    @Test
    public void testAddition() {
        assert 1 + 1 == 2;
    }

    @Test
    public void testSubtraction() {
        assert 1 - 1 == 1// This will fail
    }
}

public class TestExample {
    public static void main(String[] args) throws Exception {
        SimpleTestRunner runner = new SimpleTestRunner();
        runner.runTests(MyTests.class);
    }
}
  1. runTests 方法使用反射创建测试类的实例。
  2. 它遍历类的所有方法,检查是否有 @Test 注解。
  3. 对于有注解的方法,它使用反射来调用这些方法。

动态代理

还有就是 Java 动态代理机制,也是强依赖反射实现的。

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

interface MyInterface {
    void doSomething();
}

class MyInterfaceImpl implements MyInterface {
    public void doSomething() {
        System.out.println("Doing something");
    }
}

class MyInvocationHandler implements InvocationHandler {
    private Object target;

    public MyInvocationHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("Before method " + method.getName());
        Object result = method.invoke(target, args);
        System.out.println("After method " + method.getName());
        return result;
    }
}

public class DynamicProxyExample {
    public static void main(String[] args) {
        MyInterface original = new MyInterfaceImpl();
        MyInterface proxy = (MyInterface) Proxy.newProxyInstance(
            MyInterface.class.getClassLoader(),
            new Class<?>[] 
{ MyInterface.class },
            new MyInvocationHandler(original)
        )
;
        
        proxy.doSomething();
    }
}
  1. Proxy.newProxyInstance 方法使用反射动态创建一个实现了指定接口的代理类。
  2. InvocationHandlerinvoke 方法使用反射来调用原始对象的方法。

配置管理

其实就是我们前面举的那个数据库驱动的例子,根据配置文件动态加载一些配置类。

import java.io.FileInputStream;
import java.util.Properties;

public class ConfigurationExample {
    public static void main(String[] args) throws Exception {
        Properties props = new Properties();
        props.load(new FileInputStream("config.properties"));

        String className = props.getProperty("main.class");
        Class<?> clazz = Class.forName(className);
        Object instance = clazz.getDeclaredConstructor().newInstance();

        if (instance instanceof Runnable) {
            ((Runnable) instance).run();
        }
    }
}
  1. 我们从配置文件中读取类名。
  2. 使用 Class.forName 动态加载类。
  3. 使用反射创建类的实例。
  4. 如果类实现了 Runnable 接口,我们调用其 run 方法。

反射的注意事项

看了这么多例子,不得不说,反射确实是个好东西。但是,没有十全十美的存在,它还存在一些问题,需要我们权衡。

性能考虑:反射操作比直接方法调用慢,在性能敏感的场景中应谨慎使用。

安全性:反射可以访问私有成员,可能破坏封装,使用时需要注意安全便捷。

可读性:过度使用反射可能使代码难以理解和维护。

编译时类型检查:使用反射会绕过编译时类型检查,可能导致运行时错误。

异常处理:反射方法可能抛出多种异常,需要适当的异常处理。

还可以看看风筝往期文章

「差生文具多系列」Jetbrains IDEs中也能养宠物了,而且还有拳皇人物

「差生文具多系列」最好看的编程字体

我患上了空指针后遗症

一千个微服务之死

搭建静态网站竟然有这么多方案,而且还如此简单

被人说 Lambda 代码像屎山,那是没用下面这三个方法

古时的风筝,一个程序员,一个写作者。

古时的风筝
努力成为独立开发者的程序员,分享我了解的关于编程、独立开发等知识,知不不言,言无不尽
 最新文章