1 minute read

Java 高级主题

本文档总结了Java中一些高级且深入的主题,如反射、注解、动态代理等。


1. 反射与注解 (Reflection & Annotation)

这两个主题紧密相连:注解为代码提供元数据,反射则是在运行时读取和使用这些元数据的主要手段。

Part 1: 反射 (Reflection) - “程序的自我审视”

1. 什么是反射?

一句话概括:反射是在程序运行时,动态地获取任意类的信息、并能动态地调用其方法和操作其属性的能力。

通常情况下,我们写的代码在编译时就已经确定了要调用哪个类、哪个方法。而反射打破了这种静态性,它允许程序在“运行”的过程中,才去决定要操作哪个对象。

可以把它想象成 Java 程序的“说明书”和“万能工具箱”。通过反射,程序可以在运行时:

  • 审视自己:对于任意一个类,都能知道它所有的属性和方法。
  • 操作自己:对于任意一个对象,都能调用它的任意方法、修改它的任意属性(包括私有的!)。

2. 反射的核心API

反射功能主要由 java.lang.reflect 包下的几个核心类提供:

  • Class<T>: 反射的入口。JVM中每个加载的类都有一个与之对应的Class对象。通过它,可以获取类的所有信息。
    • 获取方式:String.class, object.getClass(), Class.forName("java.lang.String")
  • Field: 代表类的成员变量(属性)
    • 可以获取字段的类型、名称,并且可以读取和设置特定对象上该字段的值。
  • Method: 代表类的方法
    • 可以获取方法的名称、参数、返回类型,并且可以调用特定对象上的该方法。
  • Constructor<T>: 代表类的构造函数
    • 可以获取构造函数的参数,并且可以用它来创建新实例

关键方法对比:

  • getFields() vs getDeclaredFields(): 前者只能获取public的字段(包括父类的),后者可以获取所有在本类中声明的字段(不包括父类的)。MethodConstructorget...getDeclared...系列方法同理。
  • field.setAccessible(true): 这是反射的“魔法棒”,用于关闭访问权限检查,使得我们可以访问和修改 private 成员。

3. 反射的应用场景(重点)

你平时可能没有直接写很多反射代码,但你用的框架无时无刻不在使用它。

  • Spring/SpringBoot 框架
    • 依赖注入 (DI):当你写下 @Autowired 时,Spring 容器会启动并扫描你的类。它通过反射找到所有标记了 @Autowired 的字段,然后通过反射调用 field.set() 方法,将对应的 Bean 实例注入进去,这一切都无需你手动 newset
    • AOP (面向切面编程):通过反射和动态代理技术,在不修改你源码的情况下,为你调用的方法前后(或周围)增加日志、事务、安全等通用逻辑。
  • MyBatis/Hibernate (ORM 框架)
    • 当你执行一条 SQL 查询后,框架会得到一个 ResultSet。ORM 框架会通过反射创建你的实体类对象(如 User),然后根据 ResultSet 中的列名(如 “user_name”),通过反射找到 User 类中对应的 setUserName 方法或 userName 字段,并调用它来填充数据。
  • 动态代理 (Dynamic Proxy)
    • 这是反射的一个非常重要的应用。它允许你在不实现任何接口的情况下,创建一个代理对象来“伪装”成另一个对象,从而拦截对原始对象方法的调用。RPC 框架(如 Dubbo)的底层就大量用到了动态代理。
  • 通用工具库/序列化
    • Jackson/Gson,它们将 JSON 字符串转换为 Java 对象时,就是通过反射读取 JSON 的 key,找到 Java 类中同名的字段,然后通过反射为其设值。
    • 我们之前讨论的深拷贝,使用这类库实现也是基于反射。
  • 单元测试 (JUnit)
    • 当你运行测试时,JUnit 会通过反射找到所有被 @Test 注解标记的方法,然后逐一通过反射调用它们来执行测试用例。

Part 2: 注解 (Annotation) - “代码的标签”

1. 什么是注解?

注解(也叫元数据),是插入到代码中的一种特殊“标签”或“注释”。它本身不会对代码的执行逻辑产生任何影响

它的核心价值在于为其他程序(编译器、框架、工具)提供额外信息。注解就像一个贴在代码上的便签,上面写着“这里需要事务”、“这个字段需要自动注入”、“这个方法是个测试用例”等等。

2. 注解的原理(底层实现)

这部分比较深入,我们分两步看:

  • 编译期:当你写下一个注解,比如 @Override,编译器 javac 会检查这个注解。在编译时,一个注解本质上被当作一个特殊的接口来处理。当编译器将你的代码编译成 .class 文件时,它会把注解的信息(以及它的属性值)存储在 .class 文件的一个叫做“运行时可见注解属性表”的结构里。

  • 运行期

    1. 当 JVM 加载这个 .class 文件时,它会读取这些注解信息,并保存在内存中。
    2. 这是最关键的一步:当你通过反射调用 field.getAnnotation(MyAnnotation.class) 时,JVM 并不会返回一个普通的 MyAnnotation 对象。相反,JVM 使用动态代理技术,在运行时动态地创建了一个实现了 MyAnnotation 接口的代理对象 ($Proxy...)。
    3. 这个代理对象内部有一个 InvocationHandler,它连接着 JVM 内部存储的、关于这个字段的注解信息。
    4. 当你调用注解的方法(如 myAnnotation.value())时,实际上是调用了这个代理对象的方法。代理对象会通过 InvocationHandler 从 JVM 的内部数据结构中,找到 value 属性对应的值,然后返回给你。

简单来说,你拿到的注解对象,是一个运行时生成的、专门用来读取注解属性的代理对象。

3. 注解的作用域 (@Retention)

@Retention 是一个元注解(用来修饰其他注解的注解),它决定了注解的“生命周期”。

  • RetentionPolicy.SOURCE: 仅存在于源码中,编译时被丢弃。
    • 用途:主要用于给编译器提供信息,或者在编译期进行代码生成。
    • 例子@SuppressWarnings(告诉编译器忽略警告)、Lombok 的 @Data@Getter 等(Lombok 的注解处理器在编译期根据这些注解生成 get/set 方法,然后注解本身就被丢弃了)。
  • RetentionPolicy.CLASS: 存在于.class文件中,但运行时不可见。这是默认值。
    • 用途:在字节码层面进行操作,比如字节码增强。在常规开发中用得较少。
  • RetentionPolicy.RUNTIME: 存在于.class文件中,并且在运行时可以通过反射获取
    • 用途:这是最常用的类型,绝大部分框架(Spring, MyBatis, JUnit)都依赖它在运行时获取信息并执行相应逻辑。
    • 例子@Autowired, @Service, @Test, @Column 等。

4. 其他重要的元注解

  • @Target: 指定你的注解可以被用在什么地方(类、方法、字段、参数等)。
  • @Documented: 指定该注解信息是否会被包含在 Javadoc 中。
  • @Inherited: 允许子类继承父类的注解。

2. 动态代理 (Dynamic Proxy) - “无中生有”的艺术

在反射部分,我们提到了它的一个重要应用是动态代理。现在我们来深入挖掘它。

1. 代理模式是什么?

在聊动态代理前,先要理解代理模式 (Proxy Pattern)。它的核心思想是:为其他对象提供一种代理以控制对这个对象的访问。

代理就像一个“中介”,客户端不直接访问目标对象,而是通过访问中介,中介再把请求转给目标对象。这样做的好处是,中介可以在请求传递前后,添加一些额外的处理逻辑,比如权限校验、日志记录、性能监控、事务管理等,而目标对象完全不知道这些“加强”逻辑的存在,从而保持了其业务逻辑的纯粹性。

代理模式分为静态代理动态代理

  • 静态代理:代理类是在编译期就创建好的 (.java 文件是手写的)。一个代理类通常只为一个目标类服务。缺点是如果目标类增多,或接口方法改变,代理类也需要手动维护,很繁琐。
  • 动态代理:代理类是在程序运行时动态生成的。我们无需手写任何代理类的代码,它是在内存中“无中生有”的。这是所有现代框架的基石。

2. Java 中的两大动态代理技术:JDK 动态代理与 CGLIB

Java 生态中实现动态代理主要有两种主流方式。

方式一:JDK 动态代理

这是 Java 官方提供的、内置在 JDK 中的动态代理实现。

  • 核心组件
    • java.lang.reflect.Proxy:用于创建代理对象的工厂类。
    • java.lang.reflect.InvocationHandler:一个接口,你需要实现它来定义代理的“拦截逻辑”。所有对代理对象方法的调用,最终都会被转发到这个 Handlerinvoke 方法上。
  • 工作原理
    1. 前提:目标类必须实现一个或多个接口。JDK 代理是面向接口的代理。
    2. Proxy.newProxyInstance() 方法接收三个参数:类加载器、目标类实现的接口数组、以及你的 InvocationHandler 实例。
    3. 该方法会在运行时动态地创建一个新的类(通常叫 $Proxy0),这个类实现了你传入的所有接口。
    4. 当你调用代理对象的任何方法时,这个调用会被 JRE 转发到你的 InvocationHandlerinvoke 方法中。
    5. invoke 方法里,你可以决定要做什么:比如在调用原始方法 (method.invoke(target, args)) 之前打印日志,在调用之后提交事务,或者甚至完全不调用原始方法。
  • 关键限制目标类必须实现接口。它无法代理一个没有实现任何接口的普通类。

方式二:CGLIB (Code Generation Library)

CGLIB 是一个强大的、高性能的代码生成库,被广泛用于弥补 JDK 动态代理的不足。Spring 框架内部就集成了 CGLIB。

  • 核心组件
    • net.sf.cglib.proxy.Enhancer:类似于 JDK 的 Proxy 类,是创建代理对象的入口。
    • net.sf.cglib.proxy.MethodInterceptor:类似于 JDK 的 InvocationHandler,你需要实现它来定义方法拦截逻辑。
  • 工作原理
    1. 前提:目标类不能是 final
    2. CGLIB 的 Enhancer 会在运行时动态地为目标类创建一个子类
    3. 这个子类会重写目标类中所有final 的方法。
    4. 当你调用代理对象(即这个子类实例)的方法时,调用会被拦截,并转发到你的 MethodInterceptorintercept 方法中。
    5. intercept 方法里,你可以执行增强逻辑,并选择性地通过 methodProxy.invokeSuper(obj, args) 来调用原始的父类方法。
  • 核心优势不需要实现接口,可以直接代理普通的类。

3. JDK 动态代理 vs. CGLIB (面试必考对比)

特性 JDK 动态代理 CGLIB
实现基础 面向接口,利用反射。 面向继承,利用字节码技术(ASM)。
核心要求 目标类必须实现接口。 目标类不能是 final 类。
代理范围 只能代理接口中定义的方法。 可以代理目标类中所有非 final 的方法。
依赖 JDK 内置,无需额外依赖。 第三方库,需要引入 cglib 依赖。
性能 <p>对象创建速度快,方法调用因涉及反射稍慢。</p><p>(但在现代高版本JDK中,性能已被极大优化,有时甚至反超CGLIB)。</p> <p>对象创建速度慢(因为要生成子类字节码)。</p><p>方法调用速度通常更快(因为它直接调用方法,而非反射)。</p>

4. 实际应用:Spring AOP 如何选择?

这是将理论与实践结合的经典面试题。

  • Spring 框架会智能地在这两者之间进行选择
    • 如果目标 Bean 实现了接口,Spring AOP 默认使用 JDK 动态代理
    • 如果目标 Bean 没有实现任何接口,Spring AOP 会自动切换到 CGLIB 来创建子类代理。
  • 你也可以通过配置 (spring.aop.proxy-target-class=true) 来强制 Spring AOP 全部使用 CGLIB 代理,即使目标 Bean 实现了接口。