🔍 IO / 反射 / 注解
BIO/NIO/AIO · 反射与动态代理 · JDK 代理 vs CGLIB · 注解原理与自定义注解
1. BIO、NIO、AIO 的区别?
- BIO(阻塞 IO):一连接一线程。read/write 阻塞线程直到数据就绪。并发高时线程爆炸。一个线程只能处理一个连接。
- NIO(非阻塞 IO):引入 Channel、Buffer、Selector。一个线程通过 Selector 同时监听多个 Channel,只有事件就绪才处理。一个线程可以处理多个连接(IO 多路复用)。
- AIO(异步 IO):真正的异步非阻塞,IO 完成后由内核回调通知(JDK7,Linux 上基于 epoll 模拟,性能优势不明显)。
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("全限定名")。
应用场景:框架的灵魂。
- Spring IoC:通过反射实例化 Bean、注入属性
- Spring AOP / MyBatis:动态代理 + 反射调用目标方法
- Jackson / Gson:反射读写对象字段
- JDBC: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:作用范围(TYPE/METHOD/FIELD/PARAMETER/CONSTRUCTOR...)@Retention:保留策略 — SOURCE(源码,如 @Override)/ CLASS(class 文件中,默认)/ RUNTIME(运行时反射可见,自定义业务注解一般用它)@Documented、@Inherited(子类继承父类注解)- JDK8 新增:
@Repeatable(可重复注解)、@Native
自定义注解 + 反射解析(模拟日志切面)
@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 也是通过反射找到注解再织入逻辑