Java 基础 & Web 面试大全
一、Java 基础
1. JVM 由几部分构成?作用是什么?
JVM(Java 虚拟机)主要由以下几部分组成:
| 组成部分 | 作用 |
|---|---|
| 类加载器(ClassLoader) | 负责 .class 字节码文件加载到 JVM 中,包括启动类加载器、扩展类加载器、应用类加载器 |
| 运行时数据区 | JVM 内存区域划分,包含:方法区、堆、虚拟机栈、本地方法栈、程序计数器 |
| 执行引擎 | 解释执行或编译执行字节码(JIT 即时编译器),负责执行指令 |
| 本地接口(JNI) | 与本地库交互,调用 C/C++ 编写的 native 方法 |
运行时数据区详解:
- 程序计数器:记录当前线程执行的字节码行号,线程私有
- 虚拟机栈:存储栈帧(局部变量表、操作数栈等),每个方法调用创建一个栈帧,线程私有
- 本地方法栈:为 Native 方法服务,线程私有
- 堆(Heap):存放对象实例和数组,所有线程共享,GC 主要管理区域
- 方法区:存储类信息、常量池、静态变量、编译后的代码,所有线程共享
2. 接口和抽象类的区别
| 对比维度 | 接口(interface) | 抽象类(abstract class) |
|---|---|---|
| 关键字 | interface | abstract class |
| 继承关系 | 多实现(implements),一个类可实现多个接口 | 单继承(extends),一个类只能继承一个抽象类 |
| 构造方法 | 不能有构造方法 | 可以有构造方法(供子类初始化使用) |
| 成员变量 | 默认 public static final(常量) | 可以是各种类型(普通变量、静态变量、常量) |
| 成员方法 | JDK8 之前只能是抽象方法;J8 支持 default/static 方法;J9 支持 private 方法 | 可以是抽象方法,也可以是非抽象方法(具体实现) |
| 访问修饰符 | 默认 public | 可用任意修饰符 |
| 设计层面 | 是一种"行为规范"的契约(can do) | 是一种模板设计、"is-a" 关系 |
| 适用场景 | 定义能力/行为规范,如 Comparable、Serializable | 相关类共享代码和属性,如模板方法模式 |
选择建议:
- 如果是一种 IS-A 关系且需要代码复用 → 抽象类
- 如果是一种 CAN-DO 关系 → 接口
3. == 和 equals 区别
| 对比项 | == | equals() |
|---|---|---|
| 基本数据类型 | 比较值是否相等 | 不适用 |
| 引用类型 | 比较内存地址(引用是否指向同一对象) | 默认比较地址(Object 类),可重写后比较内容 |
String s1 = new String("abc");
String s2 = new String("abc");
s1 == s2; // false(不同对象,地址不同)
s1.equals(s2); // true(String 重写了 equals,比较字符序列)
// 字符串常量池
String s3 = "abc";
String s4 = "abc";
s3 == s4; // true(指向常量池同一位置)
重写 equals 的原则:
- 自反性:
x.equals(x)返回 true - 对称性:
x.equals(y)↔y.equals(x) - 传递性:
x.equals(y)且y.equals(z)→x.equals(z) - 一致性:多次调用结果一致
- 非空性:
x.equals(null)返回 false - 重写 equals 时必须重写 hashCode
4. 重载(Overload)和重写(Override)的区别
| 对比项 | 重载(Overloading) | 重写(Overriding) |
|---|---|---|
| 定义 | 同一个类中多个同名方法,参数列表不同 | 子类重新定义父类的方法 |
| 发生范围 | 同一个类中 | 子类与父类之间 |
| 方法签名 | 方法名相同,参数列表不同(个数、类型、顺序) | 方法名、参数列表、返回值都相同 |
| 返回值 | 可以不同 | 必须相同或是其子类型(协变返回) |
| 访问修饰符 | 无特殊要求 | 子类不能缩小父类的访问权限 |
| 异常 | 无特殊要求 | 子类不能抛出新的或更广的检查异常 |
| 多态类型 | 编译时多态(静态绑定) | 运行时多态(动态绑定) |
// 重载示例
public void print(int a) {}
public void print(String s) {} // 参数类型不同
public void print(int a, int b) {} // 参数个数不同
// 重写示例
class Parent { public void show() {} }
class Child extends Parent {
@Override
public void show() {} // 重写父类方法
}
5. sleep() 和 wait() 的区别
| 对比项 | sleep() | wait() |
|---|---|---|
| 所属类 | Thread.sleep() | Object.wait() |
| 释放锁 | 不释放锁(持有锁继续睡) | 释放锁(进入等待队列) |
| 唤醒方式 | 时间到自动唤醒 | 需要 notify() / notifyAll() 或时间到 |
| 使用范围 | 任何地方调用 | 必须在 synchronized 块内调用 |
| 作用范围 | 当前线程 | 当前对象的等待线程 |
| 用途 | 线程暂停执行一段时间 | 线程间通信/协调 |
// sleep 示例 - 不释放锁
synchronized (lock) {
Thread.sleep(1000); // 睡眠期间其他线程无法获取 lock
}
// wait 示例 - 释放锁
synchronized (lock) {
lock.wait(); // 释放 lock,进入等待状态
}
6. StringBuilder 和 StringBuffer 的区别
| 对比项 | StringBuilder | StringBuffer |
|---|---|---|
| 线程安全 | 非线程安全(不同步) | 线程安全(synchronized 同步) |
| 性能 | 较高(无同步开销) | 较低(有同步开销) |
| 适用场景 | 单线程环境字符串拼接 | 多线程环境字符串操作 |
| JDK版本 | JDK 1.5 引入 | JDK 1.0 就存在 |
为什么不用 String 拼接? String 不可变,每次拼接都会创建新对象,循环中性能极差。
7. String 常用的方法有哪些
| 分类 | 方法 | 说明 |
|---|---|---|
| 判断 | equals() / equalsIgnoreCase() | 比较内容是否相等 |
contains() / startsWith() / endsWith() | 包含/前缀/后缀判断 | |
isEmpty() | 长度是否为0 | |
| 获取 | length() / charAt() | 长度/指定位置字符 |
indexOf() / lastIndexOf() | 第一次/最后一次出现的位置 | |
substring(int begin, int end) | 截取子串 [begin, end) | |
toCharArray() / getBytes() | 转字符/字节数组 | |
| 转换 | toUpperCase() / toLowerCase() | 大小写转换 |
trim() | 去除首尾空格 | |
replace() / replaceAll() / split() | 替换/正则替换/拆分 | |
valueOf() | 各种类型转 String(静态方法) | |
| 比较 | compareTo() / matches() | 字典序比较/正则匹配 |
join(CharSequence delimiter, ...) | 拼接字符串(JDK8+) |
8. Synchronized 和 Lock 有什么区别
| 对比项 | synchronized | Lock(ReentrantLock 等) |
|---|---|---|
| 实现层面 | JVM 层面(monitorenter/monitorexit 指令) | API 层面(java.util.concurrent.locks 包) |
| 释放锁 | 自动释放(代码块结束或异常时) | 必须手动在 finally 中 unlock() |
| 中断响应 | 不支持(不可被中断) | 支持响应中断 lockInterruptibly() |
| 公平性 | 非公平锁(不能保证获取顺序) | 可选公平/非公平(构造参数设置) |
| 条件变量 | 只能有一个 waitSet(单一条件) | 支持多个 Condition(精细控制) |
| 超时机制 | 不支持 | 支持 tryLock(timeout) 尝试获取锁 |
| 读写锁 | 不支持 | 提供 ReentrantReadWriteLock |
| 可重入性 | 可重入 | 可重入(ReentrantLock) |
使用建议: 一般情况优先使用 synchronized;需要公平锁、可中断、超时尝试、多条件变量等高级功能时使用 Lock。
9. 谈一谈你对多态的理解
多态是指同一个行为具有多种不同的表现形式。
三个必要条件:
- 继承(或实现接口)
- 重写(子类重新父类方法)
- 父类引用指向子类对象(向上转型)
Animal a = new Cat(); // 向上转型
a.makeSound(); // 调用的是 Cat 的 makeSound(动态绑定)
两种多态类型:
- 编译时多态(重载):编译阶段确定调用哪个方法
- 运行时多态(重写):运行时根据实际对象类型确定
好处:解耦 + 扩展性好 + 统一处理(开闭原则)
10. 泛型的理解,泛型存在于编译期还是运行期?
泛型本质是参数化类型,将类型作为参数传递,使代码更通用、类型安全。
核心问题答案:泛型存在于编译期,运行期会被擦除(Type Erasure)。
- 编译后的字节码中没有泛型信息,全部变为原始类型(raw type)
<T>变成 Object 或指定的边界类型- 泛型只是给编译器看的,用于编译期类型检查
List<String> list1 = new ArrayList<>();
List<Integer> list2 = new ArrayList<>();
list1.getClass() == list2.getClass(); // true!运行时类型相同(都变成了 List)
通配符:
?— 无限定通配符? extends T— 上界通配符(只能读,不能写)? super T— 下界通配符(只能写,不能读)- PECS 原则:Producer Extends Consumer Super
11. Java 八大基本数据类型及字节数
| 数据类型 | 关键字 | 字节数 | 取值范围 |
|---|---|---|---|
| 整数型 | byte | 1 | -128 ~ 127 |
| short | 2 | ±32,767 | |
| int | 4 | 约 ±21亿 | |
| long | 8 | 约 ±9.2×10¹⁸ | |
| 浮点型 | float | 4 | ±3.4×10³⁸ |
| double | 8 | ±1.7×10³⁰⁸ | |
| 字符型 | char | 2 | 0 ~ 65,535(Unicode) |
| 布尔型 | boolean | 1 | true / false |
12. 哈希冲突怎么解决的?
哈希冲突:两个不同的 key 经过 hash 计算得到相同的哈希值。四种解决方式:
- 链地址法(拉链法)⭐ — HashMap 采用的方式,冲突元素追加到链表/红黑树
- 开放定址法 — 冲突时按规则寻找下一个空位置(ThreadLocalMap 用此策略)
- 再哈希法 — 使用多个哈希函数
- 公共溢出区 — 冲突元素放到专门的溢出表中
HashMap(JDK8)做法:数组 + 链表 + 红黑树
- 链表长度 > 8 且数组长度 ≥ 64 时转为红黑树
13. HashMap 怎么判断键值的唯一性?
两步判断:
- Hash 值是否相等:先比较 hashCode()
- equals 判断:hashCode 相同再调用 equals() 比较
// HashMap 内部逻辑
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
return p.value; // 键已存在
因此:重写 equals 时必须重写 hashCode! 否则两个"相等"的对象会存到不同的桶里,导致 containsKey/find 失败。
14. HashMap 底层原理
底层结构(JDK8):数组 + 链表 + 红黑树
┌─────────────────────────────────────┐
│ Node[] table │
│ ┌─────┬─────┬─────┬─────┬─────┐ │
│ │ 0 │ 1 │ 2 │ 3 │ ... │ │ ← 数组(默认容量16)
│ └──┬──┴──┬──┴──┬──┴──┬──┴─────┘ │
│ │ │ │ │ │
│ null ●────● ● │
│ ↓ ↓ ↓ │
│ node1 node2 node3 │ ← 链表(长度<8)
│ ↓ │
│ RBTree │ ← 红黑树(长度≥8且容量≥64)
└─────────────────────────────────────┘
核心流程(put):
- 计算哈希值:
hash = key.hashCode() ^ (key.hashCode() >>> 16)(扰动函数) - 定位桶索引:
index = hash & (n-1)(n 为 2 的幂次方) - 桶为空 → 直接放入;桶不空 → 遍历链表/红黑树
- 链表长度 ≥ 8 且 table.length ≥ 64 → 树化为红黑树
- size > threshold(负载因子 0.75 × 容量)→ 扩容 resize()(2倍)
重要参数: 初始容量16、负载因子0.75、树化阈值8、退树化阈值6、最小树化容量64
15. HashMap 和 HashSet 的区别
| 对比项 | HashMap | HashSet |
|---|---|---|
| 本质 | 存储 Key-Value 键值对 | 仅存储不重复的元素 |
| 底层实现 | 数组+链表+红黑树 | 底层就是 HashMap(value 统一为 PRESENT 占位对象) |
| 唯一性判断 | 通过 key 的 hash+equals | 元素的 hash+equals |
| 主要方法 | put/get/remove/containsKey | add/remove/contains |
| 用途 | 缓存、映射、查找 | 去重、判重 |
// HashSet 源码简化版
private transient HashMap<E,Object> map;
private static final Object PRESENT = new Object();
public boolean add(E e) { return map.put(e, PRESENT) == null; }
16. HashMap 和 HashTable 的区别
| 对比项 | HashMap | HashTable |
|---|---|---|
| 线程安全 | 非线程安全 | 线程安全(方法加 synchronized) |
| null 键/值 | 允许一个 null 键、多个 null 值 | 不允许 null 键和 null 值 |
| 初始容量/扩容 | 16 / 2倍扩容 | 11 / 2n+1 扩容 |
| 哈希算法 | h ^ (h >>> 16)(扰动优化) | key.hashCode()(直接取模) |
| 推荐程度 | ✅ 推荐 | ❌ 已过时,并发场景用 ConcurrentHashMap |
17. 动态代理与静态代理区别
| 对比项 | 静态代理 | 动态代理 |
|---|---|---|
| 生成时机 | 编译期 | 运行期 |
| 代理数量 | 一对一(一个目标类一个代理类) | 一对多(一个 Handler 处理多个类) |
| 复杂度 | 简单直接 | 相对复杂(依赖反射) |
| 灵活性 | 差(修改需改源码) | 好(通用性强) |
| 应用 | 目标明确、简单的场景 | AOP 框架底层、RPC 远程调用 |
// JDK 动态代理核心代码
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;
}
);
18. JDK 代理与 CGLIB 代理的区别
| 对比项 | JDK 动态代理 | CGLIB 代理 |
|---|---|---|
| 原理 | 反射机制,基于 Proxy 类 | 字节码操作,基于 ASM 框架生成子类 |
| 要求 | 目标类必须实现接口 | 目标类不能是 final,方法也不能是 final |
| 代理对象 | 代理的是接口,生成的是接口的实现类 | 代理的是类本身,生成的是子类 |
| 性能 | 创建快,调用稍慢(反射) | 创建慢(生成字节码),调用快 |
| Spring 选择规则 | 有接口 → 默认用 JDK Proxy | 无接口 → 自动切换 CGLIB |
19. Stream 流的语法好在哪里?常用的方法有哪些
Stream 优势:声明式编程 + 函数式风格 + 链式调用 + 惰性求值 + 并行流
常用 API 分三类:
| 类型 | 方法 | 说明 |
|---|---|---|
| 创建 | stream() / parallelStream() / Stream.of() / Arrays.stream() | 创建流 |
| 中间操作(惰性) | filter() / map() / flatMap() / distinct() / sorted() / limit() / skip() / peek() | 不触发计算 |
| 终止操作(触发) | forEach() / count() / max/min() / findFirst() / anyMatch/allMatch() / reduce() / collect() | 触发计算 |
常用收集器: Collectors.toList()/toSet()/toMap()/joining()/groupingBy()/partitioningBy()
// 示例:按部门分组统计平均薪资
Map<String, Double> result = employees.stream()
.filter(e -> e.getAge() > 20)
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.averagingDouble(Employee::getSalary)
));
二、多线程 & 并发
20. 进程和线程的区别
| 对比项 | 进程(Process) | 线程(Thread) |
|---|---|---|
| 定义 | 操作系统资源分配的基本单位 | CPU 调度和执行的基本单位 |
| 资源拥有 | 拥有独立的内存空间、文件句柄等 | 共享进程的资源(堆、方法区等) |
| 独立性 | 进程间相互隔离 | 同一进程内的线程共享地址空间 |
| 通信方式 | IPC(管道、消息队列、共享内存、Socket) | 共享变量、wait/notify、BlockingQueue |
| 创建销毁开销 | 大 | 小(轻量级) |
| 崩溃影响 | 一个进程崩溃不影响其他 | 一个线程崩溃可能导致整个进程崩溃 |
类比:进程=公司,线程=公司里的员工
21. 线程的 start() 和 run() 的区别
| 对比项 | start() | run() |
|---|---|---|
| 作用 | 启动新线程,JVM 调用 run() | 普通的方法调用 |
| 线程 | 创建并启动一个新线程 | 在当前线程中执行 |
| 并行性 | 新线程和当前线程并发执行 | 顺序执行 |
| 调用次数 | 只能 start() 一次 | 可以多次调用 |
⚠️ 重复调用 start() 会抛出 IllegalThreadStateException 异常。
22. 线程池创建的七大核心参数
ThreadPoolExecutor executor = new ThreadPoolExecutor(
corePoolSize, // ① 核心线程数:常驻线程,即使空闲也不回收
maximumPoolSize, // ② 最大线程数:队列满时可创建的最大线程数
keepAliveTime, // ③ 非核心线程空闲存活时间
TimeUnit unit, // ④ 时间单位(SECONDS/MINUTES 等)
workQueue, // ⑤ 任务队列(阻塞队列)
threadFactory, // ⑥ 线程工厂(用于自定义线程命名等)
handler // ⑦ 拒绝策略(队列满且达到最大线程数时的处理)
);
四种内置拒绝策略:
| 策略 | 行为 |
|---|---|
| AbortPolicy(默认) | 抛出 RejectedExecutionException |
| CallerRunsPolicy | 由提交任务的线程自己执行(降速保护) |
| DiscardPolicy | 直接丢弃任务,不抛异常 |
| DiscardOldestPolicy | 丢弃队列中最老的任务,重新尝试提交当前任务 |
23. 线程池使用的原因
- 降低资源消耗:线程是昂贵资源(每个约 1MB 栈空间),复用避免频繁创建/销毁
- 提高响应速度:任务到达时可直接用空闲线程,无需等待创建
- 提高可管理性:统一管理线程,控制最大并发数,防止 OOM
- 提供更多功能:延迟执行、定时执行、周期执行等
24. 线程池的工作原理
提交任务
│
▼
线程数 < corePoolSize?
├── YES → 创建【核心线程】执行
└── NO ▼
队列未满?
├── YES → 任务放入【队列】等待
└── NO ▼
线程数 < maximumPoolSize?
├── YES → 创建【非核心线程】执行
└── NO → 【拒绝策略】
// 非核心线程空闲超过 keepAliveTime 后被回收
25. 线程池怎么启动线程
// 方式1:execute() - 提交 Runnable,无返回值
executor.execute(() -> System.out.println("task"));
// 方式2:submit() - 提交 Callable/Runnable,返回 Future
Future<String> future = executor.submit(() -> "result");
String result = future.get(); // 阻塞获取结果
// 方式3:invokeAll/invokeAny() - 批量提交
List<Future<String>> futures = executor.invokeAll(tasks);
注意:execute() 提交的异常会丢失;submit() 的异常保存在 Future.get() 中抛出 ExecutionException。
26. 线程间通信方式
| 方式 | 说明 | 适用场景 |
|---|---|---|
| wait/notify/notifyAll | 基于 Object 监视器,配合 synchronized | 生产者-消费者模型 |
| volatile | 保证可见性和有序性 | 状态标志位 |
| join() | 等待目标线程结束再继续 | 等待线程完成 |
| CountDownLatch | 倒计时门闩,归零放行 | 一等多场景 |
| CyclicBarrier | 循环屏障,互相等待到齐 | 分阶段并行任务 |
| Semaphore | 信号量,限制并发数量 | 限流、资源池 |
| BlockingQueue | 阻塞队列 | 生产者-消费者(最常用) |
| Future/CompletableFuture | 异步任务结果获取 | 异步编程 |
27. volatile 了解吗?如何保证线程可见性的?
volatile 是轻量级同步机制,保证可见性和有序性,但不保证原子性。
保证可见性的底层机制:
- 写 volatile 变量时:强制将工作内存的值刷新回主内存(Store 屏障)
- 读 volatile 变量时:使其他线程对应的缓存失效,从主内存重新读取(Load 屏障)
- 底层通过插入内存屏障(Memory Barrier) 实现,禁止指令重排序
局限性: count++ 不是原子操作(读→改→写三步),volatile 无法解决!需要 AtomicInteger 或 synchronized。
适用场景: 状态标志位(volatile boolean running)、DCL 双检锁单例模式。
28. 线程池一般在什么业务场景下使用的
| 场景 | 说明 | 配置建议 |
|---|---|---|
| Web 服务器 | 处理 HTTP 请求(Tomcat 线程池) | IO 密集:核数×2 左右 |
| 异步任务 | 发送邮件/短信、生成报表 | 核心 5~10,队列适中 |
| 定时任务 | 定时清理、统计数据 | ScheduledThreadPool |
| 批量数据处理 | 批量导入/导出 | 根据 CPU 核心数调整 |
| 并行计算 | 大数据集并行处理 | ForkJoinPool |
| 消息消费 | MQ 消息处理 | 根据消息量和耗时配置 |
| 数据库连接池 | 数据库连接管理 | 根据数据库承载能力 |
29. 如何让两个线程轮流执行?/ n 个线程对一批数据相加求和
两线程交替打印(三种方案)
方案 A:wait/notify(经典方案)
int flag = 1;
Object lock = new Object();
// 线程A
synchronized (lock) {
for (int i = 0; i < 10; i++) {
while (flag != 1) { lock.wait(); } // 用 while 防虚假唤醒
System.out.println("A-" + i);
flag = 2;
lock.notifyAll();
}
}
// 线程B
synchronized (lock) {
for (int i = 0; i < 10; i++) {
while (flag != 2) { lock.wait(); }
System.out.println("B-" + i);
flag = 1;
lock.notifyAll();
}
}
方案 B:Lock + Condition(最灵活,精确通知特定线程) 方案 C:CompletableFuture(最现代优雅)
n 个线程分片相加求和
public static long parallelSum(List<Long> data, int nThreads) {
ExecutorService executor = Executors.newFixedThreadPool(nThreads);
int chunkSize = (data.size() + nThreads - 1) / nThreads; // 向上取整
List<Future<Long>> futures = new ArrayList<>();
for (int i = 0; i < nThreads; i++) {
int fromIndex = i * chunkSize;
int toIndex = Math.min(fromIndex + chunkSize, data.size());
if (fromIndex >= data.size()) break;
List<Long> subList = data.subList(fromIndex, toIndex);
futures.add(executor.submit(() -> {
long sum = 0;
for (Long num : subList) sum += num;
return sum;
}));
}
long totalSum = 0;
for (Future<Long> future : futures)
totalSum += future.get();
executor.shutdown();
return totalSum;
}
// 更优方案:LongAdder + CountDownLatch(无锁高并发累加)
30. 线程池里的异常如何处理?
问题:线程池中任务抛出的异常会被"吞掉"!
四种解决方案:
- try-catch 捕获(最直接)
- submit() + Future.get()(推荐,异常保存在 Future 中)
- 自定义 UncaughtExceptionHandler
- 包装 Runnable 统一处理
// 推荐方案:submit + Future.get
Future<?> future = executor.submit(() -> doSomethingDangerous());
try {
future.get(); // get() 时抛出 ExecutionException,getCause()拿到原始异常
} catch (ExecutionException e) {
log.error("任务失败", e.getCause());
}
// 自定义 UncaughtExceptionHandler
ExecutorService pool = Executors.newFixedThreadPool(4, r -> {
Thread t = new Thread(r);
t.setUncaughtExceptionHandler((th, ex) -> log.error("未捕获异常", ex));
return t;
});
31. 线程池中有一个线程长时间被使用无法回收,如何解决?
| 原因 | 解决方案 |
|---|---|
| 任务死循环 | 代码审查 + 设置超时 |
| 死锁 | 固定加锁顺序 + 超时检测 |
| 外部IO阻塞 | HTTP请求设超时、数据库查询设超时 |
| 第三方SDK卡住 | Future.get(timeout) + cancel(true) |
| 等待条件永不满足 | wait 加超时时间 |
防护措施:
- 任务级别:Future 超时取消
future.get(30, SECONDS); future.cancel(true) - 配置级别:设置
keepAliveTime+allowCoreThreadTimeOut(true) - 监控级别:定期检查活跃线程数、队列大小,异常时告警
- 终极手段:优雅关闭旧线程池
shutdownNow(),重建新的
什么是线程安全?
线程安全: 当多个线程同时访问同一个对象时,无论运行时环境采用何种调度方式,该对象都能表现出正确的行为。
什么导致线程不安全?(三大根源)
| 问题 | 说明 | 示例 |
|---|---|---|
| 原子性问题 | 操作不是原子的,被中途打断 | count++ 读-改-写三步操作 |
| 可见性问题 | 线程修改的值其他线程看不到 | 变量在工作内存中未刷回主存 |
| 有序性问题 | 指令重排序导致执行顺序混乱 | DCL 单例中 new 对象的重排 |
如何保证线程安全?(六种方法)
- 互斥同步:
synchronized/ReentrantLock - 非阻塞同步(CAS):
AtomicInteger/AtomicLong - 不可变对象:final 字段 + 无 setter(String、Integer)
- ThreadLocal 线程隔离:每个线程持有自己的变量副本
- 线程安全集合:ConcurrentHashMap、CopyOnWriteArrayList、BlockingQueue
- volatile:仅限状态标志位等简单场景
33. 多个线程操作同一资源的冲突处理
经典案例:库存扣减超卖问题
// ❌ 不安全:两个线程同时读到 stock=99,各减1,最终98而不是97
public void deduct() { stock--; }
// ✅ 方案1:synchronized
public synchronized void deduct() { if (stock > 0) stock--; }
// ✅ 方案2:AtomicInteger CAS 自旋
AtomicInteger stock = new AtomicInteger(100);
int prev, next;
do { prev = stock.get(); if (prev <= 0) return false; next = prev - 1; }
while (!stock.compareAndSet(prev, next));
// ✅ 方案3(分布式):数据库乐观锁 UPDATE SET stock=stock-1 WHERE id=? AND stock>0
// ✅ 方案4(分布式):Redis Lua 脚本
其他常见冲突场景:
| 场景 | 冲突表现 | 解决方案 |
|---|---|---|
| 并发计数器 | 多线程累加丢数 | AtomicLong / LongAdder |
| HashMap 并发 put | 数据丢失/死循环 | ConcurrentHashMap |
| SimpleDateFormat | 解析结果错乱 | ThreadLocal / DateTimeFormatter |
| 懒加载单例 | 重复初始化 | DCL 双检锁 + volatile |
| 生产者-消费者 | 队列为空还消费/满还生产 | wait/notify 或 BlockingQueue |
| 转账 | 并发转账余额不一致 | 行锁 + 事务 + 乐观锁 |
34. Runnable 和 Callable 的区别
| 对比项 | Runnable | Callable |
|---|---|---|
| 方法 | void run() | V call() throws Exception |
| 返回值 | ❌ 无 | ✅ 有返回值(泛型 V) |
| 异常处理 | ❌ 不能抛 checked exception | ✅ 可以抛出 checked exception |
| 提交 | execute(Runnable) | submit(Callable) → 返回 Future |
| 获取结果 | 无法获取 | future.get() 获取返回值和异常 |
35. 线程池实际应用场景 —— 1000 任务、10 集群、每台 4 核
参数设计核心公式
IO密集型:线程数 = CPU核数 × (1 + W/C) W=等待时间,C=计算时间 通常 核数×2~4
CPU密集型:线程数 = CPU核数 + 1
混合型:拆分为 IO 池和 CPU 池分别处理
IO 密集型配置(每台机器 4 核)
ThreadPoolExecutor ioPool = new ThreadPoolExecutor(
12, // 核心: 4核 × 3 = 12
24, // 最大: 4核 × 6 = 24
60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(80),
new ThreadFactoryBuilder().setNameFormat("io-task-%d").build(),
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝时自我降速执行
);
CPU 密集型配置(每台机器 4 核)
ThreadPoolExecutor cpuPool = new ThreadPoolExecutor(
5, // 4 + 1
5, // 核心 = 最大(多了没用)
60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(200), // 队列可大些
new ThreadFactoryBuilder().setNameFormat("cpu-task-%d").build(),
new ThreadPoolExecutor.CallerRunsPolicy()
);
完整生产级参考
@Configuration
public class ThreadPoolConfig {
@Bean("businessExecutor")
public ThreadPoolExecutor businessExecutor() {
int cores = Runtime.getRuntime().availableProcessors();
return new ThreadPoolExecutor(
cores * 2, cores * 4, // 根据任务类型调整倍数
60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1024),
r -> {
Thread t = new Thread(r);
t.setName("biz-" + t.getId());
t.setUncaughtExceptionHandler((th, ex) -> log.error("异常", ex));
return t;
},
new ThreadPoolExecutor.CallerRunsPolicy() // 推荐:自我保护策略
);
}
}
参数设计黄金法则:
- 队列不要无限大(容易 OOM),也不要太小(频繁拒绝)
- 生产环境推荐 CallerRunsPolicy(拒绝时由调用线程执行,实现自我降速)
- 监控活跃线程数和队列深度,设置告警阈值
36. 三个任务 a/b/c:a b 同时运行,任一完成后 c 只运行一次
推荐方案:CompletableFuture.anyOf(最优雅)✅
ExecutorService executor = Executors.newFixedThreadPool(3);
CompletableFuture<Void> futureA = CompletableFuture.runAsync(() -> {
System.out.println("任务 A 开始 " + LocalDateTime.now());
Thread.sleep(new Random().nextInt(3000));
System.out.println("任务 A 完成 " + LocalDateTime.now());
}, executor);
CompletableFuture<Void> futureB = CompletableFuture.runAsync(() -> {
System.out.println("任务 B 开始 " + LocalDateTime.now());
Thread.sleep(new Random().nextInt(3000));
System.out.println("任务 B 完成 " + LocalDateTime.now());
}, executor);
// anyOf: 任一完成即触发后续,且只触发一次!
CompletableFuture.anyOf(futureA, futureB).thenRunAsync(() -> {
System.out.println("A/B 任一已完成 → 执行任务 C!");
System.out.println("任务 C 执行完毕");
}, executor);
备选方案:CountDownLatch + AtomicBoolean
CountDownLatch latch = new CountDownLatch(1); // 门闩计数=1
AtomicBoolean cExecuted = new AtomicBoolean(false); // 保证c只执行一次
// 任务A/B 完成后:
if (cExecuted.compareAndSet(false, true)) {
latch.countDown(); // 第一个完成的才触发
}
latch.await(); // 主线程等待
// 执行任务C...
37. 怎么获取某一个方法的参数?
(1)反射获取方法的参数信息(编译时需加 -parameters)
Method method = MyClass.class.getMethod("myMethod", String.class, int.class);
Parameter[] params = method.getParameters();
for (Parameter param : params) {
System.out.println("参数名: " + param.getName()); // 需要 -parameters 编译选项
System.out.println("参数类型: " + param.getType());
}
(2)Spring AOP 拦截获取调用时的实际参数值 ⭐ 最常用
@Aspect
@Component
public class LogAspect {
@Around("execution(* com.example.service.*.*(..))")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
Object[] args = joinPoint.getArgs(); // 获取所有参数值
MethodSignature sig = (MethodSignature) joinPoint.getSignature();
String[] paramNames = sig.getParameterNames(); // 获取参数名
for (int i = 0; i < args.length; i++) {
System.out.printf("%s = %s%n", paramNames[i], args[i]);
}
return joinPoint.proceed(args);
}
}
(3)拦截器中获取 Controller 方法参数
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) {
HandlerMethod hm = (HandlerMethod) handler;
MethodParameter[] params = hm.getMethodParameters();
for (MethodParameter param : params) {
System.out.println("参数名: " + param.getParameterName());
RequestParam rp = param.getParameterAnnotation(RequestParam.class);
if (rp != null)
System.out.println(rp.value() + "=" + request.getParameter(rp.value()));
}
return true;
}
38. 什么是 OOP、API、JAR
| 概念 | 全称 | 含义 | 示例 |
|---|---|---|---|
| OOP | Object-Oriented Programming | 面向对象编程:以对象为核心,封装、继承、多态三大特性。Java 是典型的 OOP 语言 | 类、对象、接口 |
| API | Application Programming Interface | 应用程序编程接口:软件组件之间交互的约定和工具集合。可以是一个类的方法列表,也可以是 HTTP 接口 | JDK API、REST API、微信支付 API |
| JAR | Java ARchive | Java 归档文件:将多个 .class 文件和资源文件打包成一个压缩包,方便分发和部署。本质是 ZIP 格式 | spring-boot-app.jar、commons-lang3.jar |
三者关系:
- 用 OOP 思想编写代码 → 编译成
.class文件 → 打包成 JAR 文件 → 通过 API 提供功能给外部使用
39. 有没有研究过什么技术的底层,具体说说
这是一道开放性面试题,考察候选人的技术深度和学习热情。以下是常见的高分回答方向:
推荐回答方向:
| 技术方向 | 底层知识点 | 关键词 |
|---|---|---|
| HashMap | 数组+链表+红黑树、哈希扰动函数、扩容机制、树化/退树化条件 | hash扰动、2次幂、负载因子0.75、CAS无锁扩容(JDK8+) |
| 线程池 | 七大参数工作流程、ctl 高3位存状态低29位存数量、Worker 继承 AQS | ctl变量、worker创建流程、拒绝策略 |
| Spring IOC | BeanFactory vs ApplicationContext、Bean生命周期、循环依赖三级缓存 | refresh()、三级缓存(singletonFactories)、AOP代理时机 |
| Spring AOP | 动态代理(JDK Proxy/CGLIB)、切面链式调用、通知顺序 | JoinPoint、Advice chain、ProxyFactory |
| MySQL | B+Tree索引结构、MVCC、Redo Log/Undo Log、锁机制(行锁/间隙锁/临键锁) | InnoDB、聚簇索引、ReadView、WAL机制 |
| JVM | 内存模型、GC算法(G1/ZGC)、类加载双亲委派、JIT编译 | 堆栈结构、GC Roots、SafePoint、逃逸分析 |
| Redis | SDS、跳跃表(SkipList)、单线程事件驱动模型、持久化(AOF/RDB) | I/O多路复用(epoll)、Reactor模式 |
| Tomcat | 连接器(Container/Processor)、Servlet生命周期、NIO模型 | Acceptor→Poller→Worker、Connector架构 |
| MyBatis | SqlSession、动态SQL解析、一级缓存/二级缓存、插件(Interceptor) | Configuration、MappedStatement、Executor |
回答模板:
"我研究过 HashMap 的底层源码。它底层是数组+链表+红黑树的结构。put 操作时先通过 hashCode 做扰动运算定位桶位置,冲突时用拉链法解决。JDK8 引入了红黑树优化,当链表长度超过 8 且数组长度超过 64 时会转成红黑树,查找效率从 O(n) 提升到 O(log n)。另外它的扩容是 2 倍扩容..."
三、网络 & Web
40. HTTP 规范 / HTTP 协议是什么?
HTTP(HyperText Transfer Protocol) 是超文本传输协议,一种应用层协议,用于在 Web 浏览器和 Web 服务器之间传递数据。
核心特点:
- 基于 请求-响应 模式(Request-Response)
- 无状态:服务器不保存客户端上下文(每次请求独立)
- 明文传输(HTTP),HTTPS 则加密
- 默认端口:80(HTTP)、443(HTTPS)
HTTP 报文结构:
请求报文(客户端 → 服务器):
POST /api/login HTTP/1.1\r\n ← 请求行(方法 URL 版本)
Host: www.example.com\r\n ← 请求头(Key: Value)
Content-Type: application/json\r\n
Content-Length: 25\r\n
\r\n ← 空行
{"user":"admin","pw":"123"} ← 请求体
响应报文(服务器 → 客户端):
HTTP/1.1 200 OK\r\n ← 状态行(版本 状态码 状态文本)
Content-Type: application/json\r\n ← 响应头
Content-Length: 15\r\n
\r\n ← 空行
{"code":0,"msg":"ok"} ← 响应体
HTTP 各版本演进:
| 版本 | 特点 |
|---|---|
| HTTP/1.0 | 每个请求一个 TCP 连接(短连接) |
| HTTP/1.1 | 长连接(Keep-Alive)、管线化(pipelining) |
| HTTP/2 | 多路复用(一个连接并行多个流)、头部压缩、服务端推送 |
| HTTP/3 | 基于 QUIC(UDP),解决队头阻塞问题 |
41. WebSocket 连接方式、原理、如何实现
是什么?
WebSocket 是一种全双工、持久化的通信协议,建立在 TCP 之上,服务器可以主动向客户端推送消息。
与 HTTP 的关系
WebSocket 通过 HTTP 握手建立连接,之后升级为 WebSocket 协议独立通信:
握手过程:
客户端 → 服务器: GET /ws HTTP/1.1
Upgrade: websocket ← 协议升级请求
Connection: Upgrade
Sec-WebSocket-Key: xxx ← 安全密钥
Sec-WebSocket-Version: 13
服务器 → 客户端: HTTP/1.1 101 Switching Protocols ← 101 状态码:协议切换成功
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: yyy ← 服务端确认密钥
之后:双方通过 TCP 长连接双向发送 WebSocket 帧
Java 实现(Spring Boot)
// 1. 服务端
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Autowired
private MyWebSocketHandler handler;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(handler, "/ws")
.setAllowedOrigins("*"); // 允许跨域
}
}
@Component
public class MyWebSocketHandler extends TextWebSocketHandler {
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
session.sendMessage(new TextMessage("收到: " + message.getPayload()));
}
}
// 2. 前端 JS
const ws = new WebSocket('ws://localhost:8080/ws');
ws.onmessage = (event) => console.log(event.data);
ws.send('Hello Server!');
适用场景:实时聊天、股票行情推送、在线协作、游戏实时同步、运维监控大屏
42. HTTP 与 TCP 的区别
| 对比项 | HTTP | TCP |
|---|---|---|
| 层级 | 应用层(OSI 第7层) | 传输层(OSI 第4层) |
| 职责 | 规定了数据的内容格式和语义 | 负责可靠的数据传输(分段、重组、确认重传) |
| 连接 | 无状态,基于请求-响应 | 有状态,面向连接(三次握手建立连接) |
| 数据格式 | 文本格式的请求头+请求体 | 二进制字节流(Segment) |
| 可靠性 | 本身不可靠,依赖下层 TCP 保证 | 可靠传输(确认应答、超时重传、滑动窗口) |
| 关系 | HTTP 运行在 TCP 之上 | HTTP 是 TCP 的应用层协议之一 |
类比理解:TCP 就像电话线路(保证通话质量),HTTP 就是打电话时说的语言(约定了说什么怎么说)。
43. TCP 和 UDP 的区别
| 对比项 | TCP(传输控制协议) | UDP(用户数据报协议) |
|---|---|---|
| 连接性 | 面向连接(三次握手、四次挥手) | 无连接(直接发) |
| 可靠性 | 可靠(确认、重传、排序) | 不可靠(尽力送达,不保证) |
| 有序性 | 保证数据有序到达 | 不保证顺序 |
| 速度 | 较慢(有确认开销) | 快(开销小) |
| 资源消耗 | 大(维护连接状态) | 小 |
| 首部开销 | 20~60 字节 | 仅 8 字节 |
| 传输方式 | 面向字节流 | 面向报文(保留消息边界) |
| 拥塞控制 | 有(慢启动、拥塞避免) | 无 |
| 流量控制 | 有(滑动窗口机制) | 无 |
| 适用场景 | 文件传输、网页浏览、邮件、数据库连接 | 视频/语音直播、DNS查询、游戏、物联网 |
44. HTTP 与 WebSocket 的区别
| 对比项 | HTTP | WebSocket |
|---|---|---|
| 通信模式 | 半双工(请求-响应,客户端主动发起) | 全双工(双向同时通信) |
| 连接方式 | 短连接或长连接但每次需要请求 | 持久连接(一次握手后长期保持) |
| 服务器推送 | ❌ 不支持(只能轮询/SSE模拟) | ✅ 原生支持(随时推送) |
| 协议标识 | http:// / https:// | ws:// / wss:// |
| 数据格式 | 纯文本(请求头+请求体) | 二进制帧(Frame) |
| 状态码 | 使用 HTTP 状态码 | 无状态码概念 |
| 性能 | 每次携带完整 Header 头 | 连接后仅传输少量帧头 |
| 适用场景 | RESTful API、网页加载 | 实时聊天、推送通知、协同编辑 |
替代方案对比:
- 轮询:客户端定时问"有新消息吗?" — 浪费资源
- SSE(Server-Sent Events):服务器单向推送 — 比 HTTP 好,但不能客户端发
- WebSocket:真正的双向实时通信 — 最佳选择
45. HTTP 与 HTTPS 的区别
| 对比项 | HTTP | HTTPS |
|---|---|---|
| 全称 | HyperText Transfer Protocol | Secure HTTP(安全套接字层上的 HTTP) |
| 端口 | 80 | 443 |
| 安全性 | ❌ 明文传输,易被窃听篡改 | ✅ 加密传输(对称加密+非对称加密) |
| 证书 | 不需要 | 需要 SSL/TLS 证书(CA颁发) |
| 性能 | 快(无需加密解密) | 稍慢(SSL握手 + 加解密计算) |
| SEO | 相对较低排名 | Google 搜索排名加权 |
| 成本 | 免费 | 证书费用(Let's Encrypt 免费可用) |
HTTPS 工作原理(简化版):
1. 客户端 → 服务器: 发送支持的加密算法套件 + 随机数
2. 服务器 → 客户端: 返回选定的加密算法 + 证书(含公钥) + 随机数
3. 客户端验证证书(CA签名有效性),生成【预主密钥】,用【公钥加密】后发送给服务器
4. 服务器用【私钥解密】获得预主密钥
5. 双方用 随机数+预主密钥 生成相同的【会话密钥】(对称密钥)
6. 之后使用【会话密钥】进行对称加密通信(高效)
一句话总结:HTTPS = HTTP + SSL/TLS 加密层,用非对称加密交换密钥,用对称加密传输数据。
46. HTTP 响应状态码常见的都有哪些
| 分类 | 状态码 | 含义 | 常见场景 |
|---|---|---|---|
| 1xx 信息 | 100 Continue | 继续 | 大文件上传前的确认 |
| 2xx 成功 | 200 OK | 请求成功 | 正常的 GET/POST 响应 |
| 201 Created | 创建成功 | POST 新建资源成功 | |
| 204 No Content | 成功无内容 | DELETE 删除成功 | |
| 3xx 重定向 | 301 Moved Permanently | 永久重定向 | 网站换域名、HTTP→HTTPS |
| 302 Found | 临时重定向 | 登录后跳转首页 | |
| 304 Not Modified | 未修改(走缓存) | 浏览器缓存命中 | |
| 4xx 客户端错误 | 400 Bad Request | 请求语法错误 | 参数校验失败 |
| 401 Unauthorized | 未认证/未登录 | Token 过期 | |
| 403 Forbidden | 无权限 | IP被禁止、权限不足 | |
| 404 Not Found | 资源不存在 | 接口路径错误 | |
| 405 Method Not Allowed | 请求方法不允许 | 用 GET 访问需要 POST 的接口 | |
| 408 Request Timeout | 请求超时 | 客户端长时间未发送完请求 | |
| 409 Conflict | 冲突 | 并发更新同一资源 | |
| 429 Too Many Requests | 请求过于频繁 | 接口被限流 | |
| 5xx 服务器错误 | 500 Internal Server Error | 服务器内部错误 | 代码抛异常、空指针 |
| 502 Bad Gateway | 网关错误 | Nginx 后端服务挂了 | |
| 503 Service Unavailable | 服务不可用 | 服务器过载、维护中 | |
| 504 Gateway Timeout | 网关超时 | 后端处理太慢 |
记忆技巧:
- 200 万事 OK
- 301 永久搬家、302 临时出门
- 400 你说错了(参数错)、401 你没证件(没登录)、403 有证但不让你进(没权限)、404 这儿没人(找不到)
- 500 我错了(服务端异常)、502 我帮你联系的人挂了(网关后端挂了)、504 我帮你联系的人太慢了
47. Cookie 和 Session 有什么区别
| 对比项 | Cookie | Session |
|---|---|---|
| 存储位置 | 浏览器端(本地) | 服务器端(内存/Redis/数据库) |
| 安全性 | ⚠️ 较低(可被篡改/窃取) | ✅ 较高(敏感数据存在服务端) |
| 存储容量 | 约 4KB(每个 Cookie) | 理论上无限制(受服务器内存限制) |
| 有效期 | 可设过期时间(关闭浏览器也可保留) | 通常关闭浏览器失效(也可设长) |
| 跨域支持 | 可设 Domain/Path 跨子域名 | 一般不支持跨域(Token方案可跨域) |
| 服务器压力 | 无(数据在客户端) | 有(大量用户占用内存) |
| 本质 | 一段文本数据 | 服务端的数据结构 + 一个 SessionID |
工作机制:
首次访问:
Client → Server: GET /login
Server → Client: Set-Cookie: JSESSIONID=abc123 ← 服务器下发Cookie(包含Session ID)
后续请求:
Client → Server: Cookie: JSESSIONID=abc123 ← 每次请求自动带上
Server: 根据 abc123 从 Session 存储中找到对应用户信息
分布式 Session 问题:
- 多台服务器时,用户 A 在 Server1 登录,下次请求可能到 Server2(没有该用户的 Session)
- 解决方案:Session 共享存储(Redis)、Session Sticky(粘滞会话)、JWT Token(无状态方案)
48. GET 和 POST 的区别
| 对比项 | GET | POST |
|---|---|---|
| 用途 | 获取资源(查) | 提交数据(增/改/删) |
| 参数位置 | URL Query String(?key=value) | Request Body(请求体) |
| 参数可见性 | ⚠️ 明文显示在URL(浏览器历史/日志可见) | ✅ 在 Body 中(相对隐蔽) |
| 长度限制 | 有限制(URL 最大约 2048 字符,浏览器不同而异) | 无限制(理论上) |
| 安全性 | 低(参数暴露、容易被 CSRF) | 较高(不在 URL 中) |
| 缓存 | ✅ 可被缓存 | ❌ 默认不缓存(除非显式设置) |
| 幂等性 | 幂等(多次请求结果相同) | 非幂等(多次提交可能创建多条记录) |
| 书签收藏 | ✅ 可以收藏为书签 | ❌ 不行 |
| 回退/刷新 | 无影响 | ⚠️ 浏览器会提示重新提交表单 |
| 编码类型 | application/x-www-form-urlencoded | 支持多种(form-data、json、binary 等) |
| TCP 包 | 发送 1 个 TCP 包(Header+Data一起) | 先发 Header(100 Continue),再发 Data(2 个包) |
注意:从规范上说 GET 和 POST 本质相同——都是 TCP 上发送 HTTP 请求。上述区别更多是浏览器和服务器的约定俗成。
49. 过滤器(Filter)和拦截器(Interceptor)的区别
| 对比项 | Filter(过滤器) | Interceptor(拦截器) |
|---|---|---|
| 所属层面 | Servlet 规范(Java EE 标准) | Spring MVC 框架(Spring 特有) |
| 作用范围 | 所有进入容器的请求(包括静态资源) | 只拦截 Controller 的请求(DispatcherServlet 分发后的) |
| 实现接口 | javax.servlet.Filter | org.springframework.web.servlet.HandlerInterceptor |
| 配置方式 | @WebFilter 或 web.xml | @Component + addInterceptors() 注册 |
| 注入能力 | ❌ 不能直接注入 Spring Bean(需通过 SpringContextHelper) | ✅ 可以注入任何 Spring Bean |
| 执行顺序 | 在 Servlet 之前(请求最先到达) | 在 DispatcherServlet 之后、Controller 之前 |
| 适用场景 | 编码转换、CORS跨域、XSS防护、请求日志、权限过滤(粗粒度) | 权限校验(细粒度)、日志记录、性能统计、参数预处理 |
执行顺序图:
请求进来:
Browser → Tomcat Container
│
▼
【Filter】① doFilter() — 编码、CORS、XSS
│
▼
DispatcherServlet (Spring前端控制器)
│
▼
【Interceptor】② preHandle() — 登录校验、权限判断
│
▼
Controller → Service → DAO
│
▼
【Interceptor】③ postHandle() — 视图渲染前
│
▼
【Interceptor】④ afterCompletion() — 视图渲染后(清理资源)
│
▼
【Filter】⑤ FilterChain继续 — 响应输出
│
▼
Response → Browser
代码示例:
// Filter 示例
@WebFilter(urlPatterns = "/*")
public class CorsFilter implements Filter {
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) {
HttpServletResponse response = (HttpServletResponse) res;
response.setHeader("Access-Control-Allow-Origin", "*");
chain.doFilter(req, res);
}
}
// Interceptor 示例
@Component
public class AuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler) {
String token = req.getHeader("Authorization");
if (!JwtUtil.verify(token)) {
res.setStatus(401);
return false; // 拦截请求
}
return true; // 放行
}
}
// 注册拦截器
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired private AuthInterceptor authInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authInterceptor)
.addPathPatterns("/api/**") // 拦截路径
.excludePathPatterns("/api/login/**"); // 排除路径
}
}