Java 反射综合实战案例

# Java 反射

反射定义&为什么要学习反射

反射定义

反射(Reflection)指在编译的时候不需要确定数据的类型,而是延迟到运行时获取类的信息(如类名、方法名、字段名)、调用类的方法、访问或修改类的字段,而无需在编译时确定这些信息,反射使Java这种静态编译型的语言具有了动态性。

反射具有看透类的能力,类的信息在反射面前都是透明的(包括private的属性和方法都是可以调用)

Java 的反射机制主要由 java.lang.reflect 包提供支持,包括 Class 类、Method 类、Field 类等。通过这些类,可以动态地加载类、创建类实例、调用类的方法、访问和修改类的字段等操作。

为什么要学习反射

框架:反射、泛型、注解

1、扩展性和灵活性:通过反射机制,可以实现动态加载类、调用方法、访问字段等操作,从而使程序具有更强的扩展性和灵活性。这在开发框架、插件系统或通用库时非常有用。

2、框架设计:许多 Java 框架(如Spring框架)都广泛使用了反射机制,通过反射可以实现依赖注入、AOP编程等功能。

3、框架源码阅读:许多开源框架和库中广泛使用了反射,学习反射可以帮助更好地理解和阅读这些框架的源码。

4、编写通用工具类:通过反射可以编写一些通用的工具类,比如序列化/反序列化工具、注解处理器、配置文件解析器等。

虽然反射机制有很多优点,但也需要注意反射可能带来的性能开销和安全隐患。因此,在使用反射时需要谨慎考虑,并避免滥用。总的来说,学习反射机制有助于提高 Java 程序员的编程技能和应用开发能力。

反射的缺点

尽管反射机制提供了很大的灵活性和扩展性,但也存在一些缺点和不足之处,包括:

1、性能开销:使用反射会导致性能开销较大,因为在运行时需要动态地解析类结构、查找方法或字段等,比直接调用方法要慢很多。这对性能要求高的应用可能不太适用。

2、编译时检查失效:由于反射是在运行时进行的,因此编译器无法对反射代码进行类型检查,容易引入类型错误,造成运行时异常。let v;

3、安全性问题:通过反射可以访问和修改类的私有字段和方法,这可能会绕过类的封装性,造成安全隐患,特别是在安全敏感的应用中需要格外小心。

4、可读性和维护性差:使用反射的代码通常比直接调用更难理解和维护,因为它隐藏了类的结构和行为,使得代码更加晦涩难懂。

5、性能优化困难:由于反射是在运行时动态进行的,因此很难进行静态分析和优化,使得程序的性能优化变得更加困难。

6、不利于代码重构:使用反射的代码通常依赖于类的具体实现细节,一旦类结构发生变化,可能导致反射代码出错,不利于代码的重构和演化。

综上所述,虽然反射机制为 Java 程序员提供了很多便利,但在实际应用中需要谨慎使用,权衡利弊。在性能要求高、安全性要求严格或代码可维护性重要的情况下,建议尽量避免过度依赖反射机制,寻找其他更合适的解决方案。

image-20260804200912839

反射中涉及到的4个核心类

  1. Class 代表加载到内存的 .class 字节码文件,一个类在内存中只有一份Class对象,是所有反射操作的前提。
  2. Field 代表类的成员属性,通过反射实现属性读写操作;前提:获取Field对象。
  3. Method 代表类的成员方法,通过反射调用方法;前提:获取Method对象。
  4. Constructor 代表类的构造方法,通过反射创建对象;前提:获取Constructor对象。

image-20260804201028466

获取Class类对象的三种方式

//方式1:通过对象实例获取
Student student = new Student();
Class c1 = student.getClass();

//方式2:类名.class(编译期即可获取)
Class c2 = Student.class;

//方式3:静态方法 Class.forName("全类名"),运行时加载(框架最常用)
//全类名:com.study.text1.Student,经常配置在properties配置文件
Class c3 = Class.forName("com.study.text1.Student");

反射创建对象

操纵无参数构造方法(两种方式)

public static void main(String[] args) throws Exception{
    //1. 获取Class对象
    Class c = Class.forName("com.study.text1.Student");

    //方式1:直接调用 newInstance() 【调用默认无参构造】
    Object o = c.newInstance();
    System.out.println(o instanceof Student);

    //方式2:先获取Constructor构造器对象,再创建实例
    Constructor constructor1 = c.getConstructor();
    Object o1 = constructor1.newInstance();
    System.out.println(o1 instanceof Student);
    
    //等价普通代码:Student student = new Student();
}

操纵有参构造方法创建对象

public static void main(String[] args) throws Exception{
    Class c = Class.forName("com.study.text1.Student");
    //获取带两个参数的构造器,参数填写对应类型class
    Constructor constructor2 = c.getConstructor(int.class,String.class);
    //传入实参创建对象
    Object o2 = constructor2.newInstance(12,"zs");
    System.out.println(o2.toString());
    //等价普通代码:Student student = new Student(12,"zs");
}

反射操纵对象属性

Class c = Class.forName("com.study.text1.Student");
Object o = c.newInstance();

// getField():只能获取 public 修饰的属性
Field name1 = c.getField("name");

// getDeclaredField():可以获取任意权限(private/protected/public)属性
Field name = c.getDeclaredField("name");
//获取全部对象
Field[] fields = clazz1.getDeclaredFields();
// 暴力访问,解除private权限限制
name.setAccessible(true);

// 设置属性值
name.set(o,"tom");
// 获取属性值
System.out.println(name.get(o));

反射调用对象方法

Class c = Class.forName("com.study.text1.Student");
Object o = c.newInstance();

// 参数1:方法名,后面依次填写方法形参的类型Class
Method method = c.getMethod("study", Object.class);
// invoke(对象,方法实参) 执行方法
method.invoke(o,"java");

完整案例:配置文件 + 反射解耦

config.properties 配置文件

class=com.study.text1.Student
method=study

Student实体类

public class Student {
    private Integer age;
    private String name;

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

    public Student() {
    }

    public void setAge(int age) {
        this.age = age;
    }

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

    public int getAge() {
        return age;
    }

    public String getName() {
        return name;
    }

    @Override
    public String toString() {
        return "Student{" +
                "age=" + age +
                ", name='" + name + '\'' +
                '}';
    }

    public void study(Object study) {
        System.out.println("jiaoxue"+study);
    }
}

测试主程序(读取配置、反射运行)

public static void main(String[] args) throws Exception {
    //传统写法:硬编码,修改代码需要重新编译
    // Student student = new Student();
    // student.study();

    Properties properties = new Properties();
    properties.load(new FileInputStream("config.properties"));

    //读取配置文件
    String className = properties.getProperty("class");
    String methodName = properties.getProperty("method");

    //获取Class对象
    Class c = Class.forName(className);
    //反射创建对象(调用无参构造)
    Object o = c.newInstance();

    //获取目标方法
    Method method = c.getMethod(methodName, Object.class);
    //反射执行方法
    method.invoke(o,"zs");
}

Java 反射综合实战案例

练习1:反射 + 配置文件 动态加载数据库实现类

image-20260804201851284

需求:面向接口编程,只书写接口,不硬编码 new 具体数据库实现类;读取配置文件中的全类名,通过反射创建对象,切换数据库只需要修改配置文件即可。

1. 数据库顶层接口

public interface IDB {
    void getConnection();
}

2. 各个数据库实现类

// MySQL 实现
public class MySql implements IDB {
    @Override
    public void getConnection() {
        System.out.println("MySql.getConnection()");
    }
}

// Oracle 实现
public class Oracle implements IDB {
    @Override
    public void getConnection() {
        System.out.println("Oracle.getConnection()");
    }
}

// SqlServer 实现
public class SqlServer implements IDB {
    @Override
    public void getConnection() {
        System.out.println("SqlServer.getConnection()");
    }
}

3. 配置文件 db1.properties

放在项目 resources 根目录,内容如下:

className=com.xxx.MySql
# 切换数据库只需修改全类名即可
# className=com.xxx.Oracle
# className=com.xxx.SqlServer

4. 反射加载主程序

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.util.Properties;

public class DBDemo {
    public static void main(String[] args) throws Exception {
        // 1. 获取当前类的类加载器
        ClassLoader classLoader = DBDemo.class.getClassLoader();
        // 2. 通过类加载器读取resources下配置文件
        InputStream inputStream = classLoader.getResourceAsStream("db1.properties");
        Properties properties = new Properties();
        properties.load(inputStream);

        // 3. 读取配置中的实现类全限定类名
        String className = properties.getProperty("className");
        // 4. 反射获取Class对象
        Class<?> clazz = Class.forName(className);
        // 5. 获取无参构造器
        Constructor<?> constructor = clazz.getConstructor();
        // 6. 反射实例化对象,向上转型为接口
        IDB db = (IDB) constructor.newInstance();
        // 7. 调用方法
        db.getConnection();
    }

    public void db(IDB db) {
        db.getConnection();
    }
}

特点

  1. 开闭原则:新增数据库实现类时,无需修改 Java 代码,只新增实现类 + 修改配置文件;
  2. 全程只依赖 IDB 接口,实现解耦。

练习2:通用工具方法,将任意对象所有 String 属性中的字符 a 替换为 b

需求:编写通用方法 changeField(Object obj),传入任意对象,自动遍历所有成员变量,把所有 String 字段里的 a 替换成 b。

1. 实体类 Teacher

public class Teacher {
    private String name = "android";
    private int age = 18;
    private String address = "qingdao";

    @Override
    public String toString() {
        return "Teacher [name=" + name + ", age=" + age + ", address=" + address + "]";
    }
}

2. 测试代码 + 反射工具方法

import org.junit.Test;
import java.lang.reflect.Field;

public class ReflectTest {

    @Test
    public void test1() throws IllegalAccessException {
        Teacher teacher = new Teacher();
        System.out.println(teacher);
        changeField(teacher);
        System.out.println(teacher);
    }

    /**
     * 通用方法:任意对象,把所有String属性中的 a 替换成 b
     */
    private void changeField(Object object) throws IllegalAccessException {
        // 1. 获取对象运行时Class
        Class<?> clazz = object.getClass();
        // 2. 获取本类所有成员变量(包含private私有属性)
        Field[] fields = clazz.getDeclaredFields();

        // 3. 遍历所有字段
        for (Field field : fields) {
            // 只处理 String 类型属性
            if (field.getType() == String.class) {
                // 突破私有访问权限,否则会报 IllegalAccessException 访问拒绝异常
                field.setAccessible(true);

                // 获取属性原有值
                String oldValue = (String) field.get(object);
                if (oldValue != null && !oldValue.isEmpty()) {
                    // 替换字符
                    String newValue = oldValue.replace("a", "b");
                    // 重新赋值给对象属性
                    field.set(object, newValue);
                }
            }
        }
    }
}

运行结果

Teacher [name=android, age=18, address=qingdao]
Teacher [name=bndroid, age=18, address=qingdbo]

异常说明

java.lang.IllegalAccessException: 无法访问 private 修饰的成员

解决方案:

field.setAccessible(true); // 暴力反射,开启私有属性访问权限

核心思路

  1. obj.getClass() 获取运行时类型,实现任意对象通用;
  2. getDeclaredFields() 拿到全部属性,包含私有;
  3. 判断字段类型为 String 才执行替换;
  4. setAccessible(true) 解决私有属性无法读写的问题。

核心知识点总结

  1. 反射可以在运行时期获取类的结构、创建对象、读写私有成员变量;
  2. setAccessible(true):暴力反射,破除 private 权限限制;
  3. 配置文件 + 反射是工厂模式、Spring IOC 底层核心思想;
  4. getDeclaredFields():获取当前类所有字段;getFields() 只能拿到 public 字段。

关键总结

  1. getConstructor() 只能获取public构造器;私有构造需要使用 getDeclaredConstructor()
  2. getMethod() 获取public方法;getDeclaredMethod() 获取所有权限方法
  3. 访问私有属性/私有方法必须:setAccessible(true) 开启暴力反射
  4. 反射优势:解耦,只修改配置文件,不用修改Java代码、不用重启编译(Spring底层核心原理)
上一篇