CodeGym /课程 /JAVA 25 SELF /反射与依赖注入(Dependency Injection)

反射与依赖注入(Dependency Injection)

JAVA 25 SELF
第 62 级 , 课程 4
可用

1. 通过反射进行序列化

你已经知道,序列化是把对象转换为字节流或文本表示的过程。Java 中有标准序列化(Serializable),但更常见的是通过 JacksonGson 等库使用 JSON/XML 格式。

为什么这里需要反射?
要序列化对象,需要获知其字段及其值。字段可能是 private,普通代码无法访问——而反射可以。因此 JSON 类库会动态遍历对象结构,利用 FieldMethod 读取/写入字段。

示例:将对象简单序列化为字符串

数据类:

public class Person {
    private String name;
    private int age;
    private boolean active;

    public Person(String name, int age, boolean active) {
        this.name = name;
        this.age = age;
        this.active = active;
    }
}

基于反射的最简序列化器:

import java.lang.reflect.Field;

public class SimpleSerializer {
    public static String serialize(Object obj) {
        StringBuilder sb = new StringBuilder();
        Class<?> clazz = obj.getClass();
        sb.append(clazz.getSimpleName()).append("{");
        Field[] fields = clazz.getDeclaredFields();
        for (int i = 0; i < fields.length; i++) {
            Field field = fields[i];
            field.setAccessible(true); // 允许访问私有字段
            try {
                sb.append(field.getName()).append("=")
                  .append(field.get(obj));
            } catch (IllegalAccessException e) {
                sb.append(field.getName()).append("=<?>");
            }
            if (i < fields.length - 1) sb.append(", ");
        }
        sb.append("}");
        return sb.toString();
    }
}

用法:

Person p = new Person("Alice", 30, true);
System.out.println(SimpleSerializer.serialize(p));
Person{name=Alice, age=30, active=true}

它是如何工作的?

  • 通过 getDeclaredFields() 获取声明的字段。
  • 通过 setAccessible(true) 使其可访问。
  • 读取字段名和值并拼接成字符串。

示例的限制:没有处理嵌套对象、集合、数组和循环引用——这只是演示原理。

成熟库是如何做到的?

Jackson/Gson 能处理嵌套对象和集合,支持注解(@JsonIgnore@SerializedName)、日期格式等——这一切都建立在反射之上。

Jackson 示例:

import com.fasterxml.jackson.databind.ObjectMapper;

Person p = new Person("Bob", 25, false);
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(p);
// {"name":"Bob","age":25,"active":false}

2. Dependency Injection (DI) 与反射

Dependency Injection 是一种模式,依赖由外部“注入”,而不是在类内部创建。这使得代码更灵活、可测试且可扩展。在 Java 中,SpringGuiceDagger 等框架通过注解(如 @Autowired@Inject)标注注入点来实现。

为什么这里需要反射?
DI 容器需要在运行时查找字段/构造器、读取注解并创建实例——这些都通过 Class/Constructor/Field API 完成。

示例:基于反射的迷你 DI

注入注解:

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Inject {
}

含有依赖的类:

public class Service {
    public void doWork() {
        System.out.println("Service is working!");
    }
}

public class Client {
    @Inject
    private Service service;

    public void useService() {
        service.doWork();
    }
}

迷你容器:

import java.lang.reflect.*;

public class MiniDIContainer {
    // 按类创建对象,并向带有 @Inject 的字段注入依赖
    public static Object createObject(Class<?> clazz) throws Exception {
        Object obj = clazz.getDeclaredConstructor().newInstance();

        for (Field field : clazz.getDeclaredFields()) {
            if (field.isAnnotationPresent(Inject.class)) {
                Object dependency = createObject(field.getType()); // 递归地
                field.setAccessible(true);
                field.set(obj, dependency);
            }
        }
        return obj;
    }
}

用法:

public class Main {
    public static void main(String[] args) throws Exception {
        Client client = (Client) MiniDIContainer.createObject(Client.class);
        client.useService(); // Service is working!
    }
}

它如何工作?容器查找带有 @Inject 的字段,根据其类型创建依赖,并通过反射把它们设置到私有字段。

重要:这只是简化方案。真实的 DI 容器支持作用域、单例、配置、代理、处理循环依赖等。

3. 动态代理

代理是一种“替身”对象,用来拦截调用并添加行为:日志、安全、事务等。在 Java 中,这由 java.lang.reflect.ProxyInvocationHandler 完成。它是许多 Spring AOP 能力、Mockito 中的 mock 等的基础。

示例:记录日志的代理

public interface HelloService {
    void sayHello(String name);
}
public class HelloServiceImpl implements HelloService {
    public void sayHello(String name) {
        System.out.println("Hello, " + name + "!");
    }
}
import java.lang.reflect.*;

public class LoggingProxy {
    @SuppressWarnings("unchecked")
    public static <T> T createProxy(T target, Class<T> iface) {
        return (T) Proxy.newProxyInstance(
            iface.getClassLoader(),
            new Class<?>[]{iface},
            new InvocationHandler() {
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    System.out.println("调用方法: " + method.getName());
                    return method.invoke(target, args);
                }
            }
        );
    }
}

用法:

HelloService original = new HelloServiceImpl();
HelloService proxy = LoggingProxy.createProxy(original, HelloService.class);

proxy.sayHello("World");
调用方法: sayHello
Hello, World!

它如何工作?Proxy.newProxyInstance 会创建一个实现接口的对象,所有方法调用都会进入 InvocationHandler.invoke,你可以在委托前/后执行任意“横切”代码。

4. 真实场景与限制

反射在哪些“实战”场景中使用?

  • JUnit — 查找带有 @Test 的方法并通过反射调用它们。
  • Spring — 创建 bean、注入依赖、扫描注解、生成代理。
  • Jackson/Gson — 在序列化/反序列化时甚至会读取私有字段。
  • Hibernate — 根据类结构构建 ORM 模型,管理字段和代理对象。
  • Mockito — 通过代理创建 mock 并拦截调用。

为什么并非总是应该使用反射?

  • 性能。通常比普通调用更慢(可通过缓存/字节码生成缓解)。
  • 安全性。破坏封装;可访问私有数据。
  • Java 9+ 的模块化。若未显式打开包/模块,可能出现 InaccessibleObjectException

5. 常见错误

错误 1:忽略受检异常。反射方法会抛出 NoSuchFieldExceptionIllegalAccessExceptionInvocationTargetException 等。请处理它们或包装成你自己的异常。

错误 2:setAccessible(true) 并非总是有效。在 Java 9+ 的模块化应用中可能发生 InaccessibleObjectException。需要 JVM/模块参数(--add-opens)或使用公共 API。

错误 3:DI 中的循环依赖。在天真的递归中(A 依赖 B,而 B 又依赖 A)会导致 StackOverflowError。真实容器会跟踪依赖图,并使用专门技术来解决循环。

错误 4:对象序列化不完整。若没有递归遍历,引用类型的字段会被序列化为 ClassName@hash。要正确序列化,需要处理嵌套对象/集合并防止循环。

错误 5:性能损失。频繁的反射操作(在循环中、热点路径上)会成为瓶颈。使用缓存 Field/Method、字节码生成、MethodHandle 或注解处理来优化。

错误 6:破坏封装。通过反射修改私有字段会导致脆弱性和难以定位的 bug。尽量优先使用公开契约。

1
调查/小测验
反射第 62 级,课程 4
不可用
反射
反射和动态能力
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION