1. BIO、NIO、AIO 的区别?

NIO 的核心:多路复用。Selector 注册感兴趣的事件(OP_READ / OP_WRITE / OP_ACCEPT),select() 阻塞等待任一事件就绪,再逐个处理。Netty 就是对 NIO 的封装增强。

🎯 面试要点

  • Java NIO 的 select 在 Linux 上是 epoll 实现(JDK 通过 poll/selector 机制,Netty 可用 epoll 原生传输)
  • NIO 编程复杂(半包/粘包问题),业务上直接用 Netty
  • Tomcat 8.5+ 默认 NIO 模式;Servlet 3.0 起支持异步处理
  • 面试延伸:阻塞/非阻塞 vs 同步/异步是两个维度,别混为一谈

2. 什么是反射?应用场景和优缺点?

反射:运行期动态获取类的完整信息(类名、字段、方法、构造器、注解),并动态创建对象、调用方法的能力。核心 API:Class、Field、Method、Constructor。获取 Class 的三种方式:类名.class、obj.getClass()、Class.forName("全限定名")。

应用场景:框架的灵魂。

优缺点:优点——运行时解耦,写通用框架;缺点——性能损耗(跳过编译检查)、破坏封装、可能绕过访问控制。

反射基础用法
// 获取 Class
Class<?> clazz = Class.forName("com.example.User");

// 创建实例(要求有无参构造)
Object obj = clazz.getDeclaredConstructor().newInstance();

// 访问私有字段(setAccessible 绕过访问检查)
Field f = clazz.getDeclaredField("name");
f.setAccessible(true);
f.set(obj, "云雾");

// 调用方法
Method m = clazz.getMethod("greet", String.class);
m.invoke(obj, "Java");

🎯 面试要点

  • 反射性能优化:缓存 Method/Field 引用复用,避免每次 getMethod(Spring 内部就有反射缓存)
  • JDK 高版本对 setAccessible 有模块化限制(非法访问警告),框架多用 MethodHandles
  • JIT 对反射调用有优化(unreflect),热路径性能没那么差

3. JDK 动态代理和 CGLIB 的区别?

对比项 JDK 动态代理 CGLIB
要求目标必须实现接口无需接口,继承目标类
原理运行时生成接口的实现类(Proxy)生成目标类的子类,重写方法
限制只代理接口方法final 类/方法无法代理
性能JDK8+ 反射优化后与 CGLIB 相当传统略快,高版本差距小

Spring 的选择策略:目标实现接口 → 用 JDK 动态代理;没实现接口 → 用 CGLIB。Spring Boot 2.x+ 默认强制 CGLIB(即使有接口),因为 CGLIB 不需要接口约束、代理更完整(能代理所有非 final 方法)。

JDK 动态代理:InvocationHandler
UserService target = new UserServiceImpl();
UserService proxy = (UserService) Proxy.newProxyInstance(
        target.getClass().getClassLoader(),
        target.getClass().getInterfaces(),
        (proxyObj, method, args) -> {
            System.out.println("前置增强...");   // 如开启事务
            Object result = method.invoke(target, args);  // 调用真实方法
            System.out.println("后置增强...");   // 如提交事务
            return result;
        });

🎯 面试要点

  • AOP 的实现基础就是动态代理;@Transactional 失效的经典原因之一:内部方法调用绕过代理(this 调用)
  • Spring 代理对象自调用不走代理 → 事务失效;用注入自身代理或 AopContext.currentProxy()
  • jdk 代理生成的类 implements 接口 + extends Proxy,所以接口方法才能代理

4. 注解的原理?如何自定义一个注解?

本质:注解是一种特殊的接口(编译为 interface 并继承 java.lang.annotation.Annotation),通过 @interface 声明。它本身不包含逻辑,只是元数据,必须配合反射(或编译期处理)才有意义。

元注解(标注注解的注解):

自定义注解 + 反射解析(模拟日志切面)
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@interface LogExec {
    String module() default "";
    boolean enabled() default true;
}

// 使用
@LogExec(module = "订单")
public void createOrder() { ... }

// 解析:运行时反射读取注解
for (Method m : clazz.getDeclaredMethods()) {
    LogExec ann = m.getAnnotation(LogExec.class);
    if (ann != null && ann.enabled()) {
        System.out.println("记录日志:" + ann.module() + "-" + m.getName());
    }
}

🎯 面试要点

  • @Override 是 SOURCE 保留(编译期检查);Spring 的 @Component 等是 RUNTIME
  • 注解字段类型受限:基本类型、String、Class、枚举、注解、以上数组
  • 框架的注解本质都是"标记 + 反射解析",AOP 中的 @Transactional 也是通过反射找到注解再织入逻辑

🎤 常见面试追问

  1. 反射慢吗?怎么优化?——反射比直接调用慢(方法查找 + 安全检查 + 可能装箱),但现代 JVM 有优化(unreflect、方法句柄)。实践优化:缓存 Method/Field 对象复用,避免每次 getMethod。
  2. Spring 为什么 Boot 2.x 起默认用 CGLIB 代理?——代理更完整(能代理非接口方法)、无需强制接口设计、Spring 团队优化后性能与 JDK 代理相当。
  3. @Transactional 的注解是怎么"生效"的?——AOP 拦截器通过反射找到方法上的 @Transactional 注解(元数据),决定是否开启事务——注解本身没有魔法,是"标记 + 反射读取 + 代理织入"。
  4. NIO 的"非阻塞"是什么意思?——read 没有数据时立即返回而不是干等;配合 Selector 多路复用,一个线程可以监听成千上万连接的事件。
  5. 为什么 AIO 没流行起来?——Linux 上 Java AIO 底层用 epoll 模拟(不是真异步),性能优势不明显;Netty 基于 NIO(多路复用)已足够好。真异步要等 io_uring。

📖 名词解释(本页术语)

术语 大白话解释
BIO(阻塞 IO)传统 IO:read 没数据就一直等(阻塞线程),一个连接一个线程。简单但并发高时线程爆炸。
NIO(非阻塞 IO)非阻塞 + 多路复用:一个线程用 Selector 监听很多连接,有事件才处理。Netty 的基础。
AIO(异步 IO)内核把数据拷贝完成后才通知应用,应用全程不参与等待。Java 实现依赖系统支持,用得少。
反射(Reflection)运行期动态查看/操作类的机制:拿到 Class 对象 → 创建实例、调方法、读写字段。框架(Spring/MyBatis)的灵魂。
动态代理运行时生成代理对象拦截方法调用:JDK 动态代理(基于接口)和 CGLIB(基于继承)两种。
InvocationHandlerJDK 动态代理的核心接口:所有代理方法的调用都进它的 invoke(),在里面做增强(如开事务)。
注解(Annotation)代码上的"标签"(@Override/@Component),本身无逻辑,靠反射/编译期处理发挥作用。@Retention 决定保留到哪一步。
元注解标注注解的注解:@Target(用在哪)、@Retention(保留到什么时候)、@Inherited、@Repeatable。
Select 多路复用一个线程同时"等"多个连接的 IO 事件,谁有事件处理谁——NIO 高并发的核心机制。
⚠️ 本页面由 AI 生成,内容仅供参考,请以官方文档和实际源码为准。