首页
壁纸
统计
友链
Search
1
Vue2详细笔记
68 阅读
2
ExpressAPI
34 阅读
3
微信小程序
31 阅读
4
Spring MVC 请求生命周期
30 阅读
5
Nestjs概述-中文
29 阅读
Nodejs
Vue
Java
Msql
登录
Search
Wasnl
累计撰写
40
篇文章
累计收到
1
条评论
首页
栏目
Nodejs
Vue
Java
Msql
页面
壁纸
统计
友链
搜索到
18
篇与
的结果
2026-04-10
JUnit 5 单元测试完整笔记
JUnit 5 单元测试完整笔记一、JUnit 5 概述JUnit 5 是 Java 生态中最主流的单元测试框架,提供丰富的注解、断言和扩展机制。1.1 框架构成(三个子项目)子项目说明JUnit Platform在 JVM 上启动测试框架的基础平台,定义了 TestEngine APIJUnit Jupiter包含 JUnit 5 全新的编程模型和扩展机制JUnit Vintage提供对 JUnit 3 和 JUnit 4 测试的兼容运行支持二、核心测试注解JUnit 5 通过注解控制测试执行流程与行为。注解说明使用场景@Test标记一个方法为测试方法基本单元测试@BeforeEach在每个测试方法执行前运行测试数据准备、对象初始化@AfterEach在每个测试方法执行后运行资源清理、状态重置@BeforeAll在当前测试类的所有测试方法执行前运行一次(需为 static)类级别初始化(如建立数据库连接)@AfterAll在所有测试方法执行后运行一次(需为 static)类级别资源释放@DisplayName为测试类或方法自定义易读的显示名称提升测试报告的可读性@Disabled临时禁用某个测试方法或类跳过尚未实现或需要暂时忽略的测试@ParameterizedTest声明参数化测试方法使用多组数据驱动同一测试逻辑@RepeatedTest重复执行指定次数的测试验证方法稳定性或简单压力测试@Timeout指定测试方法执行超时时间性能验证、防止死循环三、断言方法(Assertions)JUnit 5 提供多种断言来验证测试结果。方法用途assertEquals(expected, actual)断言期望值与实际值相等assertTrue(condition) / assertFalse(condition)断言条件为真 / 假assertThrows(ExceptionType.class, executable)断言执行代码会抛出指定异常assertAll(executables...)组合多个断言,所有断言都会执行再报告失败(避免因一个失败而中断后续验证)四、参数化测试数据源通过 @ParameterizedTest 结合数据源注解,用多组数据测试同一逻辑。注解说明@ValueSource提供简单值数组(如 ints = {1, 2, 3})@CsvSource提供 CSV 格式的字符串数据,支持多列参数@MethodSource引用一个返回 Stream / Iterable 的静态方法提供测试数据@EnumSource使用枚举类的所有值或指定子集作为测试数据五、测试编写原则与最佳实践5.1 好的测试实践(推荐)✅ 示例代码@Test @DisplayName("当输入两个正数时应该返回正确的和") void shouldReturnSumWhenAddingTwoPositiveNumbers() { // Arrange - 准备测试数据 Calculator calculator = new Calculator(); // Act - 执行被测试的方法 int result = calculator.add(2, 3); // Assert - 验证结果 assertEquals(5, result); }✅ 核心原则AAA 模式:Arrange(准备) → Act(执行) → Assert(断言)命名清晰:测试方法名应具有描述性,表达“测什么”和“预期结果”使用 @DisplayName:进一步提高测试报告可读性单一职责:每个测试方法只验证一个功能点覆盖边界条件与异常:不仅要测试正常路径,还要测试空值、零值、负数、越界等异常场景5.2 应该避免的实践(反模式)❌ 反面示例@Test void test1() { // 测试多个不相关的功能 assertEquals(5, calculator.add(2, 3)); assertEquals(6, calculator.multiply(2, 3)); assertEquals("hello", stringUtils.reverse("olleh")); }❌ 错误做法测试方法名无意义(如 test1、testAdd)一个测试方法中验证多个不相关的功能点测试之间存在执行顺序或数据依赖忽略边界条件与异常分支测试代码逻辑过于复杂或包含大量重复代码六、常用测试类型类型说明基本功能测试验证方法在正常输入下的预期行为边界值测试测试输入为 null、空字符串、0、最大值、最小值等边界情况异常测试使用 assertThrows 验证错误输入时是否正确抛出异常参数化测试使用多组数据驱动同一测试逻辑,减少重复代码性能测试结合 @Timeout 或性能测试框架验证执行效率七、测试编写注意事项测试独立性:每个测试应能单独运行,不依赖其他测试的执行结果或执行顺序。数据自包含:测试数据应在测试方法内部准备,避免使用共享可变全局状态。断言消息:为关键断言添加描述性消息,便于快速定位失败原因。资源清理:在 @AfterEach 或 @AfterAll 中释放资源(如文件流、数据库连接)。代码简洁:保持测试代码易读、易维护,必要时可提取私有辅助方法。八、测试覆盖率目标指标建议值整体代码覆盖率≥ 80%核心业务逻辑重点覆盖,尽量达到 90% 以上公共方法所有 public 方法均应有对应测试异常分支与边界条件必须包含相关测试用例覆盖率工具使用 JaCoCo 等工具生成报告并持续监控笔记总结完毕。 按照以上结构和内容复习,即可系统掌握 JUnit 5 的使用方法与单元测试最佳实践。
2026年04月10日
10 阅读
0 评论
0 点赞
2026-04-09
并发编程基础概念
一、并发编程基础概念1.1 并发与并行并发:多任务交替执行(单核/多核),依赖 CPU 时间片轮转。宏观上是同时进行的,微观上是串行。并行:多任务同时执行(必须多核),利用多个 CPU 核心同时处理不同任务。1.2 Java 内存模型 (JMM)主内存:所有线程共享,存储实例变量、静态变量。工作内存:每个线程私有,存储主内存变量的副本。线程不能直接操作主内存,必须先读入工作内存,操作完成后再写回。JMM 定义的 8 种内存交互操作(原子指令):lock(锁定):作用于主内存,将变量标识为线程独占。unlock(解锁):作用于主内存,释放独占状态。read(读取):作用于主内存,将变量值传输到工作内存。load(载入):作用于工作内存,将 read 的值放入变量副本。use(使用):作用于工作内存,将变量值传递给执行引擎。assign(赋值):作用于工作内存,将执行引擎的值赋给变量副本。store(存储):作用于工作内存,将变量值传送到主内存。write(写入):作用于主内存,将 store 的值写入主内存变量。1.3 线程安全问题的根源与表现根源:主内存与工作内存数据不一致。竞态条件:结果依赖线程执行的相对时序(如 check-then-act)。数据竞争:多线程访问同一内存,至少一个写操作,且无同步。死锁:两个及以上线程互相持有对方需要的锁,永久阻塞。二、并发编程三大特性(详细版)特性详细描述实现手段可见性线程 A 修改变量后,线程 B 能立即看到变化。volatile、synchronized、Lock(锁释放前刷新主存)、final原子性一个或多个操作要么全执行,要么全不执行,中间不被调度中断。synchronized、Lock、java.util.concurrent.atomic 原子类有序性避免指令重排序带来的逻辑错误。单线程内语义表现为串行。volatile(内存屏障)、synchronized(锁释放与获取原则)三、线程基础 API 详解3.1 创建线程的方式(含代码细节)方式一:继承 Thread 类public class ThreadExample extends Thread { @Override public void run() { System.out.println("Thread is running"); } public static void main(String[] args) { ThreadExample t = new ThreadExample(); t.start(); } }方式二:实现 Runnable 接口public class RunnableExample implements Runnable { @Override public void run() { System.out.println("Runnable task is running"); } public static void main(String[] args) { Thread t = new Thread(new RunnableExample()); t.start(); // Lambda 简化写法 new Thread(() -> System.out.println("Lambda style")).start(); } }方式三:实现 Callable 接口(有返回值、可抛异常)import java.util.concurrent.Callable; import java.util.concurrent.FutureTask; public class CallableExample implements Callable<String> { @Override public String call() throws Exception { Thread.sleep(1000); return "Callable result"; } public static void main(String[] args) throws Exception { FutureTask<String> futureTask = new FutureTask<>(new CallableExample()); new Thread(futureTask).start(); // 阻塞获取结果 System.out.println(futureTask.get()); } }3.2 Thread 类构造方法与核心控制构造方法细节:Thread():默认线程名 "Thread-" + n。Thread(String name):指定线程名。Thread(Runnable target):指定任务。Thread(Runnable target, String name):指定任务与名称。线程优先级与守护线程:Thread t = new Thread(); t.setPriority(Thread.MAX_PRIORITY); // 10(最高) t.setPriority(Thread.NORM_PRIORITY); // 5(默认) t.setPriority(Thread.MIN_PRIORITY); // 1(最低) // 守护线程:JVM 退出时不管守护线程是否结束 Thread daemonT = new Thread(() -> { while (true) { /* 后台任务,如日志清理 */ } }); daemonT.setDaemon(true); // 必须在 start() 前设置 daemonT.start();3.3 线程中断机制(方法签名详解)void interrupt():设置中断标志位为 true。boolean isInterrupted():检查标志位,不清除。static boolean interrupted():检查当前线程标志位,并清除(重置为 false)。Thread t = new Thread(() -> { while (!Thread.currentThread().isInterrupted()) { // 业务逻辑 try { Thread.sleep(1000); } catch (InterruptedException e) { // 捕获异常后标志位会被清除,需手动再次中断以退出循环 Thread.currentThread().interrupt(); } } });四、synchronized 锁机制深度解析4.1 锁的三种形态(代码示例)对象锁(锁当前实例 this):class SyncDemo { // 同步实例方法 public synchronized void method1() { /* 锁 this */ } // 同步代码块 public void method2() { synchronized (this) { /* 锁 this */ } } }类锁(锁 Class 对象):class SyncDemo { // 同步静态方法 public static synchronized void staticMethod1() { /* 锁 SyncDemo.class */ } public void staticMethod2() { synchronized (SyncDemo.class) { /* 锁类对象 */ } } }4.2 锁升级机制(Java 6+,不可逆)偏向锁:无竞争时,在对象头记录线程 ID,线程再次进入仅需检查 ID,无需 CAS。轻量级锁:交替执行时,通过 CAS 自旋尝试获取锁,不阻塞线程(消耗 CPU)。重量级锁:激烈竞争时,自旋失败升级为 OS 互斥量,线程挂起进入阻塞队列(开销大)。五、volatile 与内存屏障5.1 volatile 特性总结保证可见性:写 volatile 变量时强制刷新工作内存到主内存;读 volatile 变量时强制从主内存加载。禁止重排序:通过插入内存屏障防止 CPU/编译器对指令进行重排。不保证原子性:i++ 操作(读-改-写)依然不安全。5.2 内存屏障分类(JMM 规范)屏障类型作用示例场景LoadLoad屏障前读操作完成,才能执行屏障后读操作。读变量A; LoadLoad; 读变量B;LoadStore屏障前读操作完成,才能执行屏障后写操作。读变量A; LoadStore; 写变量B;StoreStore屏障前写操作完成并可见,才能执行屏障后写操作。写变量A; StoreStore; 写变量B;StoreLoad屏障前写操作完成并可见,才能执行屏障后读操作。开销最大,兼具其他三者功能。写变量A; StoreLoad; 读变量B;volatile 屏障插入策略:volatile 写:前插 StoreStore,后插 StoreLoad。volatile 读:后插 LoadLoad + LoadStore。六、Lock 接口与 ReentrantLock 详细 API6.1 Lock 接口方法清单方法详细描述void lock()阻塞获取锁,不可中断,死等。void lockInterruptibly()可响应中断的获取锁,等待时若线程被中断则抛 InterruptedException。boolean tryLock()非阻塞,获取到返回 true,否则立即返回 false。boolean tryLock(long time, TimeUnit unit)超时等待,超时返回 false;等待时可响应中断。void unlock()释放锁。必须在 finally 块中调用。Condition newCondition()返回绑定此锁的条件变量。6.2 ReentrantLock 使用模板class X { private final ReentrantLock lock = new ReentrantLock(); public void m() { lock.lock(); // block until condition holds try { // ... method body } finally { lock.unlock(); // 关键:防止死锁 } } }6.3 公平锁与非公平锁构造// 非公平锁(默认,吞吐量高) ReentrantLock unfairLock = new ReentrantLock(); ReentrantLock unfairLock2 = new ReentrantLock(false); // 公平锁(FIFO,避免线程饥饿但上下文切换多) ReentrantLock fairLock = new ReentrantLock(true);6.4 可中断与超时示例// 可中断锁示例 try { lock.lockInterruptibly(); // 临界区 } catch (InterruptedException e) { // 线程被中断,退出等待队列 } finally { lock.unlock(); } // 超时锁示例 if (lock.tryLock(2, TimeUnit.SECONDS)) { try { // 获取成功 } finally { lock.unlock(); } } else { // 执行备选方案 }七、读写锁与 StampedLock7.1 ReentrantReadWriteLock 特性读写互斥:读锁与写锁互斥。锁降级:持有写锁的线程可以获取读锁,然后释放写锁,降级为读锁。不支持锁升级:持有读锁时直接获取写锁会导致死锁,JVM 禁止此行为。ReadWriteLock rwLock = new ReentrantReadWriteLock(); Lock readLock = rwLock.readLock(); Lock writeLock = rwLock.writeLock(); // 缓存更新场景:锁降级示例 writeLock.lock(); try { // 1. 更新数据库/缓存 readLock.lock(); // 在释放写锁前获取读锁 } finally { writeLock.unlock(); // 降级为读锁 } try { // 2. 后续读取操作(保证数据可见性) } finally { readLock.unlock(); }7.2 StampedLock 三种模式(Java 8+,不可重入)写锁:long stamp = stampedLock.writeLock(); 返回邮戳。悲观读锁:long stamp = stampedLock.readLock();乐观读:无锁机制,依赖版本校验。class Point { private double x, y; private final StampedLock sl = new StampedLock(); // 乐观读(读多写少性能最优) double distanceFromOrigin() { long stamp = sl.tryOptimisticRead(); // 获取乐观读邮戳 double currentX = x, currentY = y; // 无锁读取 if (!sl.validate(stamp)) { // 校验邮戳是否失效 stamp = sl.readLock(); // 失效则升级为悲观读 try { currentX = x; currentY = y; } finally { sl.unlockRead(stamp); } } return Math.sqrt(currentX * currentX + currentY * currentY); } }八、Condition 条件变量(替换 Object Monitor)8.1 核心方法对比Condition 方法Object 对应方法说明await()wait()释放锁并等待。必须在 while 循环中调用以防虚假唤醒。signal()notify()唤醒一个等待线程。signalAll()notifyAll()唤醒所有等待线程。awaitUninterruptibly()无不可中断等待。awaitUntil(Date deadline)wait(long timeout)带截止时间等待。8.2 生产者-消费者模型(多条件队列)class BoundedBuffer { final Lock lock = new ReentrantLock(); final Condition notFull = lock.newCondition(); final Condition notEmpty = lock.newCondition(); // 防御性编程:必须用 while 循环 public void put(Object x) throws InterruptedException { lock.lock(); try { while (count == items.length) // 防止虚假唤醒 notFull.await(); // ... 入队逻辑 notEmpty.signal(); } finally { lock.unlock(); } } }九、原子类与 CAS 底层原理9.1 AtomicInteger 常用方法方法签名操作描述int get()获取当前值。void set(int newValue)设置新值(无原子性保证,仅可见性)。int getAndSet(int newValue)设置新值,返回旧值。boolean compareAndSet(expect, update)CAS 核心操作。int getAndIncrement()等价于 i++。int incrementAndGet()等价于 ++i。int getAndAdd(int delta)加 delta 并返回旧值。9.2 ABA 问题与解决方案ABA 问题描述:线程 1 读取值 A,线程 2 改为 B,线程 3 又改回 A。线程 1 使用 CAS 检查时发现值仍为 A,误以为未变化而更新成功。解决方案:AtomicStampedReference 引入版本号。// 泛型类:AtomicStampedReference<V> AtomicStampedReference<Integer> ref = new AtomicStampedReference<>(100, 0); // 初始值100,版本号0 int stamp = ref.getStamp(); int oldValue = ref.getReference(); // 更新时需同时匹配值和时间戳 boolean success = ref.compareAndSet(oldValue, 200, stamp, stamp + 1);
2026年04月09日
15 阅读
0 评论
0 点赞
2026-04-09
Java 多线程与线程池完整笔记
Java 多线程与线程池完整笔记一、线程池基础1.1 什么是线程池线程池是一种多线程处理模式,它在系统启动时预先创建一定数量的线程放入池中。这些线程在没有任务时处于等待状态,当有任务提交时,池中某个空闲线程会被唤醒执行任务。任务执行完成后,线程并不会被销毁,而是返回池中继续等待下一个任务。┌─────────────────────────────────────────────────┐ │ 线程池架构 │ ├─────────────────────────────────────────────────┤ │ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │ │ │线程1│ │线程2│ │线程3│ │线程N│ ← 常驻线程 │ │ └──┬──┘ └──┬──┘ └──┬──┘ └──┬──┘ │ │ │ │ │ │ │ │ └─────────┴────┬────┴─────────┘ │ │ ▼ │ │ ┌───────────────┐ │ │ │ 任务队列 │ ← 缓冲待处理任务 │ │ └───────────────┘ │ └─────────────────────────────────────────────────┘1.2 为什么需要线程池优势说明资源复用避免频繁创建和销毁线程的开销,线程对象可重复使用响应速度任务到达时无需等待线程创建,立即执行可管理性统一分配、调优和监控线程,防止资源耗尽流量控制通过队列和最大线程数限制并发量,保护系统二、ThreadPoolExecutor 核心参数详解2.1 七个核心参数public ThreadPoolExecutor( int corePoolSize, // 1. 核心线程数 int maximumPoolSize, // 2. 最大线程数 long keepAliveTime, // 3. 空闲线程存活时间 TimeUnit unit, // 4. 时间单位 BlockingQueue<Runnable> workQueue, // 5. 工作队列 ThreadFactory threadFactory, // 6. 线程工厂 RejectedExecutionHandler handler // 7. 拒绝策略 )参数含义比喻corePoolSize核心线程数,即使空闲也保留在池中的线程数量餐厅正式员工,永不辞退maximumPoolSize最大线程数 = 核心线程 + 临时线程正式员工 + 临时工的上限keepAliveTime临时线程空闲超过该时间则被回收临时工多久没活干就辞退unitkeepAliveTime 的时间单位秒、毫秒等workQueue存放待执行任务的阻塞队列餐厅等位区threadFactory创建线程的工厂,可自定义线程名、优先级等员工招聘标准handler队列满且线程达最大值时的拒绝策略客满时的处理方式2.2 常用工作队列类型队列类型特点适用场景SynchronousQueue不存储任务,直接交给线程执行CachedThreadPoolLinkedBlockingQueue无界队列(默认 Integer.MAX_VALUE)FixedThreadPoolArrayBlockingQueue有界队列,需指定容量需限制队列长度的场景PriorityBlockingQueue优先级队列任务有优先级要求三、线程池执行流程(五步核心逻辑)// 伪代码表示执行逻辑 public void execute(Runnable task) { // 步骤1:当前线程数 < corePoolSize if (workerCount < corePoolSize) { addWorker(task, true); // 新建核心线程执行 return; } // 步骤2:核心线程已满,尝试放入队列 if (workQueue.offer(task)) { // 放入成功,等待空闲线程取走执行 return; } // 步骤3:队列已满,尝试创建临时线程 if (workerCount < maximumPoolSize) { addWorker(task, false); // 新建临时线程执行 return; } // 步骤4:线程数已达最大值,执行拒绝策略 reject(task); }流程图:提交任务 │ ▼ 当前线程数 < corePoolSize? ──Yes──▶ 创建核心线程执行 │ No │ ▼ 任务能否入队? ──Yes──▶ 入队等待执行 │ No(队列已满) │ ▼ 当前线程数 < maximumPoolSize? ──Yes──▶ 创建临时线程执行 │ No │ ▼ 执行拒绝策略四、四种拒绝策略策略类行为比喻使用建议AbortPolicy抛出 RejectedExecutionException直接赶走客人并吵架默认策略,开发常用,快速失败CallerRunsPolicy由提交任务的线程自己执行老板亲自服务客人可减缓提交速度,重要任务兜底DiscardPolicy静默丢弃新任务悄悄赶走客人不重要的任务,如日志上报DiscardOldestPolicy丢弃队列中最老的任务赶走排队最久的人优先处理新任务// 自定义拒绝策略示例 new ThreadPoolExecutor.AbortPolicy() // 抛异常 new ThreadPoolExecutor.CallerRunsPolicy() // 调用者执行 new ThreadPoolExecutor.DiscardPolicy() // 直接丢弃 new ThreadPoolExecutor.DiscardOldestPolicy() // 丢弃最旧的五、Executors 四种快捷线程池⚠️ 开发规范:强制禁止使用 Executors 创建线程池,必须通过 ThreadPoolExecutor 构造函数显式指定参数。原因:FixedThreadPool 和 SingleThreadPool 使用无界队列,可能 OOMCachedThreadPool 和 ScheduledThreadPool 允许创建无限数量线程,可能 OOM工厂方法核心特点适用场景风险newFixedThreadPool(n)固定线程数,无界队列重负载服务器队列无限堆积newCachedThreadPool()0核心,无限临时线程,同步队列大量短期异步任务线程数爆炸newSingleThreadExecutor()单线程,无界队列任务需顺序执行队列无限堆积newScheduledThreadPool(n)支持定时/周期任务定时调度线程数爆炸// ❌ 错误示范 ExecutorService pool = Executors.newFixedThreadPool(10); // ✅ 正确示范 ExecutorService pool = new ThreadPoolExecutor( 10, 20, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(1000), new ThreadFactoryBuilder().setNameFormat("my-pool-%d").build(), new ThreadPoolExecutor.CallerRunsPolicy() );六、线程数设置公式6.1 CPU 密集型任务定义:大量时间用于计算,很少等待 I/O线程数 = CPU 核心数 + 1加 1 是为了在某个线程因缺页中断等原因暂停时,有备用线程顶上,最大化 CPU 利用率。6.2 I/O 密集型任务定义:大量时间等待 I/O 操作(网络、磁盘、数据库)线程数 = CPU 核心数 × 2更精确的公式:线程数 = CPU 核心数 × (1 + 平均等待时间 / 平均计算时间)6.3 混合型任务将任务拆分为 CPU 密集和 I/O 密集两部分,分别用不同线程池处理。七、线程池生命周期与优雅关闭7.1 五种状态状态说明接收新任务执行队列任务RUNNING正常运行✅✅SHUTDOWN关闭中(调用 shutdown())❌✅STOP强制停止(调用 shutdownNow())❌❌TIDYING过渡状态,所有任务终止❌❌TERMINATED完全终止❌❌7.2 优雅关闭代码模板public void shutdownGracefully(ExecutorService pool) { pool.shutdown(); // ① 停止接收新任务 try { // ② 等待现有任务完成,最多等待 60 秒 if (!pool.awaitTermination(60, TimeUnit.SECONDS)) { pool.shutdownNow(); // ③ 超时则强制关闭 // ④ 再等待 5 秒,确认是否终止 if (!pool.awaitTermination(5, TimeUnit.SECONDS)) { System.err.println("线程池未能正常终止"); } } } catch (InterruptedException e) { pool.shutdownNow(); // ⑤ 当前线程被中断,也强制关闭 Thread.currentThread().interrupt(); } }八、ScheduledThreadPoolExecutor 定时任务线程池8.1 概述ScheduledThreadPoolExecutor 继承自 ThreadPoolExecutor,专门用于执行定时和周期性任务,是传统 Timer 的替代品。对比项TimerScheduledThreadPoolExecutor线程模型单线程多线程池异常处理任务抛异常则 Timer 线程终止单任务异常不影响其他任务任务调度基于绝对时间基于相对时间,更精准适用场景简单定时生产级定时调度8.2 三种核心 APIScheduledExecutorService scheduler = Executors.newScheduledThreadPool(5); // 1. 延迟执行一次 ScheduledFuture<?> future = scheduler.schedule( () -> System.out.println("延迟执行"), 5, TimeUnit.SECONDS ); // 2. 固定频率执行(不管上次是否完成) scheduler.scheduleAtFixedRate( () -> System.out.println("固定频率"), 1, // 首次延迟 1 秒 3, // 每 3 秒执行一次 TimeUnit.SECONDS ); // 3. 固定延迟执行(等上次完成后再延迟) scheduler.scheduleWithFixedDelay( () -> System.out.println("固定延迟"), 1, // 首次延迟 1 秒 3, // 上次完成后等待 3 秒 TimeUnit.SECONDS );8.3 scheduleAtFixedRate vs scheduleWithFixedDelay方法调度方式效果scheduleAtFixedRate固定频率(周期固定)不管任务执行多久,严格按周期触发,可能任务堆叠scheduleWithFixedDelay固定延迟(间隔固定)等上次执行完,再等固定时间才触发,周期 = 执行时间 + 延迟scheduleAtFixedRate (周期 = 3秒) 任务执行2秒 → 等待1秒 → 下一轮 任务执行4秒 → 立即触发下一轮(无等待) scheduleWithFixedDelay (延迟 = 3秒) 任务执行2秒 → 等待3秒 → 下一轮(间隔5秒) 任务执行4秒 → 等待3秒 → 下一轮(间隔7秒)九、线程池性能监控与调优9.1 关键监控指标指标含义告警阈值排查方向活跃线程数当前正在工作的线程数> 最大线程数 80%线程数不足或任务耗时过长队列长度等待执行的任务数> 队列容量 70%消费速度跟不上生产速度任务完成数已完成任务累计增长停滞线程池可能卡死或阻塞拒绝任务数被拒绝的任务累计> 0(正常应为 0)线程数和队列容量需调大线程池大小当前池中线程总数长期接近 maximumPoolSize调大 maximumPoolSize任务等待时间任务从提交到执行的时间> 业务可接受阈值处理能力不足9.2 监控实现方案@Component public class ThreadPoolMonitor { private final ThreadPoolExecutor executor; @Scheduled(fixedDelay = 30000) // 每30秒采集一次 public void monitor() { int activeCount = executor.getActiveCount(); long completedCount = executor.getCompletedTaskCount(); int queueSize = executor.getQueue().size(); int poolSize = executor.getPoolSize(); long rejectCount = // 自定义记录 log.info("线程池监控 - 活跃:{}, 队列:{}, 池大小:{}, 已完成:{}, 拒绝:{}", activeCount, queueSize, poolSize, completedCount, rejectCount); // 告警判断 if (queueSize > queueCapacity * 0.7) { alert("队列使用率超过70%"); } } }9.3 调优策略现象可能原因解决方案队列持续增长消费速度 < 生产速度增加线程数,或优化任务执行效率活跃线程数持续满任务耗时过长拆分任务、异步化、增加最大线程数拒绝次数频繁线程池容量不足增大队列容量和最大线程数CPU 使用率低但队列满线程阻塞于 I/O增加线程数(I/O 密集型公式)OOM 内存溢出无界队列堆积改用有界队列 + 合理拒绝策略十、最佳实践总结10.1 编码规范// ✅ 正确:显式创建 ThreadPoolExecutor ThreadPoolExecutor executor = new ThreadPoolExecutor( 10, // 核心线程数 20, // 最大线程数 60L, TimeUnit.SECONDS, // 空闲超时 new ArrayBlockingQueue<>(1000), // 有界队列 new NamedThreadFactory("biz-pool"), // 自定义线程工厂 new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略 ); // ❌ 错误:使用 Executors 快捷方法 ExecutorService pool = Executors.newFixedThreadPool(10);10.2 必须设置的内容[ ] 有界队列(避免 OOM)[ ] 合适的拒绝策略(根据业务选型)[ ] 有意义的线程名称(便于排查问题)[ ] 监控指标采集(JMX / Micrometer)[ ] 优雅关闭逻辑(应用停止时调用)10.3 参数选择参考业务类型corePoolSizemaximumPoolSize队列类型队列容量计算密集型 APICPU 核数 + 1CPU 核数 + 1ArrayBlockingQueue100~500I/O 密集型 APICPU 核数 × 2CPU 核数 × 4ArrayBlockingQueue500~2000后台批量处理1~51~10LinkedBlockingQueue10000+即时响应任务10~5050~200SynchronousQueue0本笔记涵盖了 Java 多线程线程池的核心知识点,建议结合实战加深理解。
2026年04月09日
7 阅读
0 评论
1 点赞
2026-04-09
Java多线程与线程池
# Java多线程与线程池目录线程池基础再深入ThreadPoolExecutor 源码级剖析阻塞队列深度对比与选型拒绝策略详解与自定义实战线程工厂定制与线程命名规范线程池动态调整与运行时管理任务提交与结果处理进阶线程池钩子方法与扩展点ScheduledThreadPoolExecutor 深度解析线程池监控与告警体系搭建常见陷阱与最佳实践生产级线程池配置模板一、线程池基础再深入1.1 线程池的设计思想 —— 池化技术池化技术的核心是空间换时间,预先创建资源对象,使用时直接获取,用完后归还,避免频繁创建销毁的开销。除了线程池,常见的池化技术还有:数据库连接池(HikariCP、Druid)对象池(Apache Commons Pool)内存池(Netty 的 ByteBuf)1.2 线程池解决了什么问题?(细化版)问题线程池解决方案线程创建开销大线程对象复用,避免反复 new Thread().start() 的系统调用线程数量失控通过 maximumPoolSize 和队列容量双重限制并发上限任务堆积导致 OOM使用有界队列 + 拒绝策略,防止无限制接受任务线程生命周期管理复杂统一提供 shutdown() / shutdownNow() 接口任务执行情况难以追踪提供 beforeExecute / afterExecute 钩子和统计指标1.3 线程池的替代方案比较方案优点缺点new Thread(runnable).start()简单直接无复用,资源浪费,难以管理Timer支持定时单线程,异常即停,不推荐ThreadPoolExecutor生产级标准参数较多,需理解原理ForkJoinPool工作窃取,适合递归分治任务不适合普通任务CompletableFuture + 自定义线程池异步编程更优雅学习曲线稍高二、ThreadPoolExecutor 源码级剖析2.1 核心数据结构 —— ctl 原子变量ThreadPoolExecutor 使用一个 AtomicInteger 类型的 ctl 变量,同时存储两个信息:高 3 位:线程池运行状态(RUNNING、SHUTDOWN、STOP、TIDYING、TERMINATED)低 29 位:当前工作线程数量(workerCount)private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0)); private static final int COUNT_BITS = Integer.SIZE - 3; // 29 private static final int CAPACITY = (1 << COUNT_BITS) - 1; // 约 5.3 亿 // 状态常量(高3位) private static final int RUNNING = -1 << COUNT_BITS; // 111... private static final int SHUTDOWN = 0 << COUNT_BITS; // 000... private static final int STOP = 1 << COUNT_BITS; // 001... private static final int TIDYING = 2 << COUNT_BITS; // 010... private static final int TERMINATED = 3 << COUNT_BITS; // 011... // 拆包方法 private static int runStateOf(int c) { return c & ~CAPACITY; } private static int workerCountOf(int c) { return c & CAPACITY; } private static int ctlOf(int rs, int wc) { return rs | wc; }状态转换图: RUNNING │ shutdown() ▼ SHUTDOWN ──队列为空──▶ TIDYING ── terminated()──▶ TERMINATED │ │ shutdownNow() ▼ STOP ──────────────▶ TIDYING ── terminated()──▶ TERMINATED2.2 内部类 Worker —— 工作线程的封装private final class Worker extends AbstractQueuedSynchronizer implements Runnable { final Thread thread; // 实际执行任务的线程 Runnable firstTask; // 构造时可能携带的首个任务 volatile long completedTasks; // 完成任务计数 Worker(Runnable firstTask) { setState(-1); // 初始禁止中断,直到线程启动 this.firstTask = firstTask; this.thread = getThreadFactory().newThread(this); } public void run() { runWorker(this); // 核心循环 } }Worker 继承 AQS 实现了简单的非重入互斥锁,目的是:在执行任务期间,防止其他线程中断正在运行任务的 Worker(interruptIdleWorkers 只中断空闲的)锁状态表示 Worker 是否正在执行任务(state >= 1 表示忙碌)2.3 runWorker —— 线程池运行核心循环final void runWorker(Worker w) { Thread wt = Thread.currentThread(); Runnable task = w.firstTask; w.firstTask = null; w.unlock(); // 允许中断 boolean completedAbruptly = true; try { while (task != null || (task = getTask()) != null) { w.lock(); // 1. 检查线程池状态,必要时中断自己 if ((runStateAtLeast(ctl.get(), STOP) || (Thread.interrupted() && runStateAtLeast(ctl.get(), STOP))) && !wt.isInterrupted()) wt.interrupt(); try { beforeExecute(wt, task); // 钩子:任务执行前 Throwable thrown = null; try { task.run(); // 执行任务 } catch (RuntimeException x) { thrown = x; throw x; } catch (Error x) { thrown = x; throw x; } catch (Throwable x) { thrown = x; throw new Error(x); } finally { afterExecute(task, thrown); // 钩子:任务执行后 } } finally { task = null; w.completedTasks++; w.unlock(); } } completedAbruptly = false; } finally { processWorkerExit(w, completedAbruptly); } }2.4 getTask —— 从队列获取任务(包含线程销毁逻辑)private Runnable getTask() { boolean timedOut = false; for (;;) { int c = ctl.get(); int rs = runStateOf(c); // 检查是否需要减少 Worker if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) { decrementWorkerCount(); return null; } int wc = workerCountOf(c); // 是否允许核心线程超时 或 当前线程数超过核心数 boolean timed = allowCoreThreadTimeOut || wc > corePoolSize; if ((wc > maximumPoolSize || (timed && timedOut)) && (wc > 1 || workQueue.isEmpty())) { if (compareAndDecrementWorkerCount(c)) return null; continue; } try { // 根据 timed 决定是 poll(带超时)还是 take(阻塞) Runnable r = timed ? workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : workQueue.take(); if (r != null) return r; timedOut = true; } catch (InterruptedException retry) { timedOut = false; } } }关键点:非核心线程通过 poll 超时返回 null 后会被回收核心线程默认 take 无限阻塞,除非设置 allowCoreThreadTimeOut(true)三、阻塞队列深度对比与选型3.1 常用阻塞队列底层结构与特性队列底层结构锁机制容量适用场景ArrayBlockingQueue数组单锁(ReentrantLock)必须指定固定容量、内存占用可控LinkedBlockingQueue单向链表两把锁(putLock/takeLock)默认 Integer.MAX_VALUE吞吐量高、无需预知容量SynchronousQueue无存储公平模式队列/非公平栈0直接交付,CachedThreadPoolPriorityBlockingQueue二叉堆数组单锁无界任务有优先级排序DelayQueue优先队列单锁无界定时任务存储3.2 ArrayBlockingQueue vs LinkedBlockingQueue 性能差异// 压测对比结论(仅供参考) // ArrayBlockingQueue:入队出队共用一把锁,并发竞争激烈时性能下降 // LinkedBlockingQueue:入队和出队各用一把锁,吞吐量更高,但内存占用更大 // 选择建议: // - 内存敏感、容量可预估 → ArrayBlockingQueue // - 追求高吞吐、内存较充裕 → LinkedBlockingQueue(需指定容量)3.3 SynchronousQueue 的两种模式// 公平模式:TransferQueue(FIFO) new SynchronousQueue<>(true); // 非公平模式:TransferStack(LIFO)——默认 new SynchronousQueue<>();SynchronousQueue 内部没有容量,每个插入操作必须等待另一个线程的移除操作。CachedThreadPool 使用它,使得任务立即交给线程执行,不积压。四、拒绝策略详解与自定义实战4.1 JDK 内置四种策略源码分析// AbortPolicy public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { throw new RejectedExecutionException("Task " + r.toString() + " rejected from " + e.toString()); } // CallerRunsPolicy public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { if (!e.isShutdown()) { r.run(); // 直接在调用者线程执行 } } // DiscardPolicy public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { // do nothing } // DiscardOldestPolicy public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { if (!e.isShutdown()) { e.getQueue().poll(); // 丢弃队头 e.execute(r); // 重新尝试提交 } }4.2 自定义拒绝策略实战(带日志与监控)public class MonitoredCallerRunsPolicy implements RejectedExecutionHandler { private static final Logger log = LoggerFactory.getLogger(MonitoredCallerRunsPolicy.class); private final MeterRegistry meterRegistry; private final Counter rejectCounter; public MonitoredCallerRunsPolicy(MeterRegistry meterRegistry) { this.meterRegistry = meterRegistry; this.rejectCounter = Counter.builder("thread.pool.reject") .description("线程池拒绝任务计数") .register(meterRegistry); } @Override public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { rejectCounter.increment(); log.warn("线程池 {} 任务被拒绝,当前线程数: {}/{}, 队列大小: {}/{},降级为调用者执行", e.toString(), e.getPoolSize(), e.getMaximumPoolSize(), e.getQueue().size(), e.getQueue().remainingCapacity() + e.getQueue().size()); if (!e.isShutdown()) { // 在调用者线程中执行,并记录耗时(可选) long start = System.nanoTime(); try { r.run(); } finally { log.debug("降级任务执行耗时: {}ms", TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start)); } } } }4.3 其他常见自定义策略// 1. 阻塞等待策略(当队列满时,让提交线程阻塞一小段时间再尝试) public class BlockingWaitPolicy implements RejectedExecutionHandler { private final long maxWaitMs; public BlockingWaitPolicy(long maxWaitMs) { this.maxWaitMs = maxWaitMs; } @Override public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { if (!e.isShutdown()) { try { if (!e.getQueue().offer(r, maxWaitMs, TimeUnit.MILLISECONDS)) { throw new RejectedExecutionException("等待超时后仍无法提交"); } } catch (InterruptedException ex) { Thread.currentThread().interrupt(); throw new RejectedExecutionException("等待期间被中断", ex); } } } } // 2. 持久化到数据库/消息队列的策略 public class PersistencePolicy implements RejectedExecutionHandler { @Override public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { // 将任务序列化存储到 DB 或 MQ,后续补偿 TaskStorage.save((Serializable) r); log.info("任务被拒绝,已持久化待补偿"); } }五、线程工厂定制与线程命名规范5.1 自定义 ThreadFactory 模板public class NamedThreadFactory implements ThreadFactory { private final AtomicInteger threadNumber = new AtomicInteger(1); private final String namePrefix; private final boolean daemon; private final int priority; private final Thread.UncaughtExceptionHandler exceptionHandler; public NamedThreadFactory(String poolName) { this(poolName, false, Thread.NORM_PRIORITY); } public NamedThreadFactory(String poolName, boolean daemon, int priority) { this.namePrefix = poolName + "-thread-"; this.daemon = daemon; this.priority = priority; this.exceptionHandler = (t, e) -> log.error("线程 {} 发生未捕获异常", t.getName(), e); } @Override public Thread newThread(Runnable r) { Thread t = new Thread(r, namePrefix + threadNumber.getAndIncrement()); t.setDaemon(daemon); t.setPriority(priority); t.setUncaughtExceptionHandler(exceptionHandler); return t; } }5.2 使用示例ThreadPoolExecutor executor = new ThreadPoolExecutor( 10, 20, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(1000), new NamedThreadFactory("order-processor", false, Thread.NORM_PRIORITY), new ThreadPoolExecutor.CallerRunsPolicy() );5.3 守护线程与用户线程的区别类型行为适用场景用户线程(默认)JVM 等待所有用户线程结束后才退出执行业务逻辑的线程池守护线程JVM 退出时立即终止,不管是否执行完后台辅助任务(如日志刷盘)六、线程池动态调整与运行时管理6.1 可动态调整的参数// 以下方法均可在运行时调用 executor.setCorePoolSize(newCoreSize); executor.setMaximumPoolSize(newMaxSize); executor.setKeepAliveTime(newTime, TimeUnit.SECONDS); executor.setRejectedExecutionHandler(newHandler); executor.setThreadFactory(newFactory); // 仅影响后续新建线程 executor.allowCoreThreadTimeOut(true); // 允许核心线程超时回收6.2 动态调整实战:基于负载的自动扩缩容@Component public class AdaptiveThreadPoolManager { private final ThreadPoolExecutor executor; private final int minCoreSize; private final int maxCoreSize; @Scheduled(fixedDelay = 10000) public void adjustPoolSize() { int queueSize = executor.getQueue().size(); int activeCount = executor.getActiveCount(); int currentCore = executor.getCorePoolSize(); // 队列堆积严重且活跃线程接近核心数 → 增加核心线程 if (queueSize > 500 && activeCount >= currentCore * 0.9) { int newCore = Math.min(currentCore + 2, maxCoreSize); executor.setCorePoolSize(newCore); log.info("队列堆积,增加核心线程至 {}", newCore); } // 队列持续为空且核心线程数大于最小值 → 减少核心线程 if (queueSize == 0 && activeCount < currentCore * 0.3 && currentCore > minCoreSize) { int newCore = Math.max(currentCore - 1, minCoreSize); executor.setCorePoolSize(newCore); log.info("队列空闲,减少核心线程至 {}", newCore); } } }6.3 预热核心线程// 提前创建所有核心线程,避免懒加载导致初期响应慢 executor.prestartAllCoreThreads(); // 或者预热指定数量 executor.prestartCoreThread();七、任务提交与结果处理进阶7.1 execute vs submit 的区别方法返回值异常处理execute(Runnable)void异常抛出到 UncaughtExceptionHandlersubmit(Runnable)Future<?>异常封装在 Future.get() 中submit(Callable<T>)Future<T>异常封装在 Future.get() 中// execute 的异常不会被吞掉,会传播到线程的 UncaughtExceptionHandler executor.execute(() -> { throw new RuntimeException("execute异常"); }); // submit 的异常会被封装在 Future 中,必须调用 get() 才会抛出 Future<?> future = executor.submit(() -> { throw new RuntimeException("submit异常"); }); try { future.get(); // 这里会抛出 ExecutionException } catch (ExecutionException e) { e.getCause(); // 原始异常 }7.2 CompletionService —— 先完成先获取当需要处理一批任务并按照完成顺序获取结果时,使用 CompletionService 比手动遍历 Future 列表更高效。ExecutorService executor = Executors.newFixedThreadPool(10); CompletionService<String> completionService = new ExecutorCompletionService<>(executor); // 提交多个任务 List<Future<String>> futures = new ArrayList<>(); for (int i = 0; i < 100; i++) { final int taskId = i; futures.add(completionService.submit(() -> { Thread.sleep(ThreadLocalRandom.current().nextInt(1000)); return "Task-" + taskId + " result"; })); } // 按完成顺序获取结果 for (int i = 0; i < 100; i++) { Future<String> completed = completionService.take(); // 阻塞等待下一个完成的任务 String result = completed.get(); System.out.println(result); }7.3 invokeAll 与 invokeAnyList<Callable<String>> tasks = Arrays.asList( () -> { Thread.sleep(1000); return "A"; }, () -> { Thread.sleep(500); return "B"; } ); // invokeAll:等待所有任务完成,返回所有 Future List<Future<String>> allFutures = executor.invokeAll(tasks); // invokeAny:只要有一个任务成功完成就返回其结果,其他任务取消 String firstResult = executor.invokeAny(tasks); // 返回 "B"八、线程池钩子方法与扩展点8.1 三个可重写的钩子方法public class MonitoredThreadPool extends ThreadPoolExecutor { public MonitoredThreadPool(...) { super(...); } @Override protected void beforeExecute(Thread t, Runnable r) { super.beforeExecute(t, r); // 可以在此记录任务开始时间,存入 ThreadLocal TaskContext.start(t.getName(), r); } @Override protected void afterExecute(Runnable r, Throwable t) { super.afterExecute(r, t); try { if (t != null) { log.error("任务执行异常", t); metrics.recordError(); } // 记录任务执行耗时 long duration = TaskContext.getDuration(); metrics.recordTaskDuration(duration); } finally { TaskContext.clear(); } } @Override protected void terminated() { super.terminated(); log.info("线程池已完全终止"); metrics.unregister(); } }8.2 使用 ThreadLocal 传递上下文(需注意清理)public class TaskContext { private static final ThreadLocal<Long> START_TIME = new ThreadLocal<>(); private static final ThreadLocal<String> TASK_NAME = new ThreadLocal<>(); public static void start(String threadName, Runnable task) { START_TIME.set(System.nanoTime()); TASK_NAME.set(task.getClass().getSimpleName()); } public static long getDuration() { Long start = START_TIME.get(); return start == null ? -1 : TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start); } public static void clear() { START_TIME.remove(); // ⚠️ 必须清理,否则会导致内存泄漏 TASK_NAME.remove(); } }为什么必须清理 ThreadLocal?线程池中的线程是复用的,如果不在 afterExecute 中清理,ThreadLocal 中的数据会残留到下一个任务中,导致数据错乱或内存泄漏(Value 对象一直被强引用)。九、ScheduledThreadPoolExecutor 深度解析9.1 架构与继承关系ThreadPoolExecutor ▲ │ ScheduledThreadPoolExecutor │ └── 内部类 ScheduledFutureTask (实现 RunnableScheduledFuture) └── 内部类 DelayedWorkQueue (基于堆的延迟队列)9.2 DelayedWorkQueue 实现原理数据结构:二叉小顶堆,按照任务的下次执行时间排序线程等待机制:Leader-Follower 模式,只有一个 Leader 线程等待堆顶任务的到期时间,其他线程无限期等待,减少不必要的唤醒// 简化的 Leader-Follower 逻辑 public RunnableScheduledFuture<?> take() throws InterruptedException { lock.lockInterruptibly(); try { for (;;) { RunnableScheduledFuture<?> first = queue[0]; if (first == null) { available.await(); // 无任务,等待 } else { long delay = first.getDelay(NANOSECONDS); if (delay <= 0) { return finishPoll(first); // 任务到期,取出 } first = null; if (leader != null) { available.await(); // 已有 Leader,当前线程等待 } else { Thread thisThread = Thread.currentThread(); leader = thisThread; try { available.awaitNanos(delay); // 作为 Leader 等待 } finally { if (leader == thisThread) leader = null; } } } } } finally { if (leader == null && queue[0] != null) available.signal(); // 唤醒一个 Follower lock.unlock(); } }9.3 ScheduledFutureTask 关键字段private class ScheduledFutureTask<V> extends FutureTask<V> implements RunnableScheduledFuture<V> { private long time; // 下次执行时间(纳秒) private final long period; // 周期:正数 fixedRate,负数 fixedDelay,0 表示单次 private final long sequenceNumber; // 全局序号,用于任务排序 }9.4 scheduleAtFixedRate 与 scheduleWithFixedDelay 源码差异// 在 run 方法中重新设置下次执行时间 public void run() { boolean periodic = isPeriodic(); if (!canRunInCurrentRunState(periodic)) cancel(false); else if (!periodic) super.run(); else if (super.runAndReset()) { setNextRunTime(); // 关键差异点 reExecutePeriodic(outerTask); } } private void setNextRunTime() { long p = period; if (p > 0) // scheduleAtFixedRate time += p; // 基于上次计划时间累加,保持固定频率 else // scheduleWithFixedDelay time = triggerTime(-p); // 基于当前完成时间 + delay }后果对比示例:// 假设任务执行需要 4 秒,周期/延迟设为 3 秒 // scheduleAtFixedRate (period=3 > 0) // 任务1:0秒开始,4秒结束 → 下次执行时间 = 0+3=3秒(已过),立即执行任务2 // 任务2:3秒(实际4秒)开始... // scheduleWithFixedDelay (period=-3) // 任务1:0秒开始,4秒结束 → 下次执行时间 = 当前时间+3秒 = 7秒十、线程池监控与告警体系搭建10.1 基于 Micrometer 的完整监控实现@Component public class ThreadPoolMetricsBinder { private final MeterRegistry meterRegistry; private final Map<String, ThreadPoolExecutor> pools = new ConcurrentHashMap<>(); public void register(String poolName, ThreadPoolExecutor executor) { pools.put(poolName, executor); // 核心指标 Gauge Gauge.builder("thread.pool.core.size", executor, ThreadPoolExecutor::getCorePoolSize) .tag("pool", poolName).register(meterRegistry); Gauge.builder("thread.pool.max.size", executor, ThreadPoolExecutor::getMaximumPoolSize) .tag("pool", poolName).register(meterRegistry); Gauge.builder("thread.pool.active", executor, ThreadPoolExecutor::getActiveCount) .tag("pool", poolName).register(meterRegistry); Gauge.builder("thread.pool.pool.size", executor, ThreadPoolExecutor::getPoolSize) .tag("pool", poolName).register(meterRegistry); Gauge.builder("thread.pool.largest.size", executor, ThreadPoolExecutor::getLargestPoolSize) .tag("pool", poolName).register(meterRegistry); Gauge.builder("thread.pool.queue.size", executor, e -> e.getQueue().size()) .tag("pool", poolName).register(meterRegistry); Gauge.builder("thread.pool.queue.remaining", executor, e -> e.getQueue().remainingCapacity()) .tag("pool", poolName).register(meterRegistry); Gauge.builder("thread.pool.completed", executor, e -> e.getCompletedTaskCount()) .tag("pool", poolName).register(meterRegistry); // 派生计算指标(队列使用率) Gauge.builder("thread.pool.queue.usage", executor, e -> { int size = e.getQueue().size(); int capacity = size + e.getQueue().remainingCapacity(); return capacity > 0 ? (double) size / capacity : 0; }).tag("pool", poolName).register(meterRegistry); } // 定时打印监控日志(便于开发调试) @Scheduled(fixedDelay = 30000) public void logPoolStats() { pools.forEach((name, pool) -> { log.info("线程池[{}] 状态 - 核心:{}, 当前:{}, 活跃:{}, 队列:{}/{}, 完成:{}, 拒绝:{}", name, pool.getCorePoolSize(), pool.getPoolSize(), pool.getActiveCount(), pool.getQueue().size(), pool.getQueue().size() + pool.getQueue().remainingCapacity(), pool.getCompletedTaskCount(), getRejectedCount(pool) // 需自行记录 ); }); } }10.2 拒绝计数与异常计数记录public class InstrumentedThreadPool extends ThreadPoolExecutor { private final Counter rejectedCounter; private final Counter exceptionCounter; public InstrumentedThreadPool(..., MeterRegistry registry, String poolName) { super(...); this.rejectedCounter = Counter.builder("thread.pool.rejected") .tag("pool", poolName).register(registry); this.exceptionCounter = Counter.builder("thread.pool.exception") .tag("pool", poolName).register(registry); } @Override protected void afterExecute(Runnable r, Throwable t) { super.afterExecute(r, t); if (t != null) { exceptionCounter.increment(); } } // 自定义拒绝处理器,在其内部调用 rejectedCounter.increment() }10.3 告警规则配置(Prometheus + AlertManager 示例)groups: - name: thread_pool_alerts interval: 30s rules: - alert: ThreadPoolQueueHighUsage expr: thread_pool_queue_usage > 0.8 for: 5m annotations: summary: "线程池 {{ $labels.pool }} 队列使用率超过 80%" - alert: ThreadPoolRejectOccurred expr: increase(thread_pool_rejected_total[1m]) > 0 annotations: summary: "线程池 {{ $labels.pool }} 出现任务拒绝" - alert: ThreadPoolActiveThreadHigh expr: thread_pool_active / thread_pool_max_size > 0.9 for: 10m annotations: summary: "线程池 {{ $labels.pool }} 活跃线程数持续接近上限"十一、常见陷阱与最佳实践11.1 陷阱一:ThreadLocal 内存泄漏// ❌ 错误:在任务中设置 ThreadLocal 但未清理 executor.submit(() -> { ThreadLocal<HeavyObject> tl = new ThreadLocal<>(); tl.set(new HeavyObject()); // 任务结束后 tl 仍被线程持有 // 未调用 remove() }); // ✅ 正确:使用 try-finally 确保清理 executor.submit(() -> { ThreadLocal<HeavyObject> tl = new ThreadLocal<>(); try { tl.set(new HeavyObject()); // 业务逻辑 } finally { tl.remove(); } });11.2 陷阱二:线程池被多个任务共享时的并发问题// 线程池本身是线程安全的,但要注意: // 如果任务间共享可变状态,必须加锁 class SharedCounter { private int count = 0; public void increment() { count++; } // 非原子操作 } // ❌ 多个任务同时修改共享对象 SharedCounter counter = new SharedCounter(); for (int i = 0; i < 1000; i++) { executor.submit(counter::increment); } // 结果可能小于 1000 // ✅ 使用原子类或同步 class SafeCounter { private AtomicInteger count = new AtomicInteger(0); public void increment() { count.incrementAndGet(); } }11.3 陷阱三:使用无界队列导致 OOM// ❌ 致命错误 ExecutorService pool = Executors.newFixedThreadPool(10); // 队列是 LinkedBlockingQueue,默认 Integer.MAX_VALUE // 如果任务提交速度远大于处理速度,队列会无限增长直至 OOM // ✅ 使用有界队列 new ThreadPoolExecutor(10, 10, 0L, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(1000), // 指定容量 ...);11.4 陷阱四:异常被吞没// submit 的异常如果不调用 get() 会被静默吞掉 Future<?> future = executor.submit(() -> { throw new RuntimeException(); }); // 这里不会打印任何异常日志 // 解决方案: // 1. 始终调用 get() 处理异常 // 2. 使用 execute 代替 submit // 3. 在 afterExecute 中捕获并记录异常(如前面所示)11.5 陷阱五:线程池关闭后仍有任务提交executor.shutdown(); executor.execute(task); // 抛出 RejectedExecutionException11.6 最佳实践清单实践项具体做法禁止 Executors 快捷方法显式使用 ThreadPoolExecutor 构造函数有界队列 + 合理拒绝策略根据业务选择 CallerRunsPolicy 或自定义自定义线程命名使用 NamedThreadFactory监控指标全覆盖集成 Micrometer,暴露关键指标优雅关闭使用两阶段关闭模板异常处理在 afterExecute 中统一记录未捕获异常上下文传递使用 TransmittableThreadLocal 或手动清理参数动态化提供配置中心调整核心参数的能力十二、生产级线程池配置模板12.1 通用业务线程池配置@Configuration public class ThreadPoolConfig { @Bean("commonExecutor") public ThreadPoolExecutor commonExecutor() { int cpuCores = Runtime.getRuntime().availableProcessors(); ThreadPoolExecutor executor = new ThreadPoolExecutor( cpuCores * 2, // corePoolSize cpuCores * 4, // maximumPoolSize 60, TimeUnit.SECONDS, // keepAlive new ArrayBlockingQueue<>(2000), // 有界队列 new NamedThreadFactory("common-pool", false, Thread.NORM_PRIORITY), new ThreadPoolExecutor.CallerRunsPolicy() ); executor.allowCoreThreadTimeOut(true); // 空闲时回收核心线程 return executor; } }12.2 高吞吐 I/O 密集型线程池@Bean("ioIntensiveExecutor") public ThreadPoolExecutor ioIntensiveExecutor() { // 根据 I/O 等待时间调整 int coreSize = Runtime.getRuntime().availableProcessors() * 2; int maxSize = coreSize * 4; return new ThreadPoolExecutor( coreSize, maxSize, 120, TimeUnit.SECONDS, new LinkedBlockingQueue<>(5000), // 容量较大 new NamedThreadFactory("io-pool", false, Thread.NORM_PRIORITY), new BlockingWaitPolicy(100) // 自定义阻塞等待拒绝策略 ); }12.3 定时任务专用线程池@Bean("scheduledExecutor") public ScheduledExecutorService scheduledExecutor() { return new ScheduledThreadPoolExecutor( Runtime.getRuntime().availableProcessors(), new NamedThreadFactory("scheduled-pool", false, Thread.NORM_PRIORITY), new ThreadPoolExecutor.CallerRunsPolicy() ); }12.4 带有完整监控的线程池构建器public class MonitoredThreadPoolBuilder { public static ThreadPoolExecutor build(String poolName, int core, int max, int queueCapacity, MeterRegistry registry) { ThreadPoolExecutor executor = new InstrumentedThreadPool( core, max, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(queueCapacity), new NamedThreadFactory(poolName), new MonitoredCallerRunsPolicy(registry), registry, poolName ); // 注册到监控绑定器 return executor; } }附录:快速参考卡线程池参数速查表参数推荐值范围备注corePoolSizeCPU核数 ~ CPU核数×2根据任务类型调整maximumPoolSizecorePoolSize ~ corePoolSize×4不宜过大,避免上下文切换开销keepAliveTime30s ~ 120s临时线程回收时间队列容量500 ~ 10000根据任务提交速率和处理速率计算拒绝策略选择指南场景推荐策略关键业务,不可丢失CallerRunsPolicy 或持久化策略可丢弃的非核心任务DiscardPolicy新任务优先级高于旧任务DiscardOldestPolicy快速失败,开发调试AbortPolicy(默认)线程数计算公式N = CPU 核心数 U = 目标 CPU 利用率 (0 < U <= 1) W/C = 等待时间与计算时间的比值 最优线程数 = N * U * (1 + W/C)以上内容涵盖了从基础概念到源码原理、从监控告警到生产实践的完整知识体系,可作为学习和工作的全面参考资料。
2026年04月09日
15 阅读
0 评论
0 点赞
2026-04-03
SpringBoot
Spring Boot 2.7 入门到精通笔记(苍穹外卖实战版)一、Spring Boot 概述1.1 什么是 Spring BootSpring Boot 是 Spring 框架的“脚手架”,核心设计理念是 约定优于配置。与传统 Spring 的区别:对比项传统 SpringSpring Boot配置方式繁琐的 XML 配置文件注解 + 自动配置依赖管理手动管理版本、处理冲突起步依赖(Starter)自动引入部署方式打包成 WAR 部署到 Tomcat内嵌 Tomcat,直接 java -jar开发效率需要大量样板代码只需关心业务逻辑1.2 Spring Boot 核心特性自动配置:根据类路径中的依赖自动配置 Bean起步依赖:通过 Starter 机制简化依赖管理内嵌容器:默认集成 Tomcat/Jetty/Undertow生产就绪:Actuator 提供监控、健康检查等能力1.3 Spring Boot 2.7 新特性(重点)Spring Boot 2.7 是一个重要版本,2.5 已停止维护[reference:0]。自动配置变更:自动配置注册文件从 META-INF/spring.factories 迁移到 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports新增 @AutoConfiguration 注解,用于标识自动配置类[reference:1]支持 after/before 属性进行配置排序其他新特性:支持 GraphQL(spring-boot-starter-graphql)支持 Podman 容器引擎Web Server SSL 增强[reference:2]1.4 苍穹外卖项目概述苍穹外卖是一个企业级 O2O 外卖平台,分为管理端和用户端(微信小程序):角色功能管理端员工管理、菜品/套餐管理、订单管理、数据统计用户端登录授权、浏览点餐、购物车结算、订单追踪技术栈:Spring Boot 2.7 + Spring MVC + MyBatis-Plus + MySQL + Redis + OSS(阿里云)+ AOP + Swagger(Knife4j)+ WebSocket[reference:3]二、快速开始2.1 项目结构(苍穹外卖)苍穹外卖采用 Maven 多模块结构,各模块职责明确,便于维护和扩展:sky-take-out(父工程) ├── pom.xml # 统一依赖版本管理 ├── sky-common/ # 公共模块:工具类、常量、异常类 │ ├── constant/ # 常量定义(如JwtClaimsConstant) │ ├── context/ # ThreadLocal上下文 │ ├── exception/ # 自定义异常 │ ├── properties/ # 配置属性类 │ └── utils/ # 工具类(JWT、阿里OSS、微信支付等) ├── sky-pojo/ # 实体模块:Entity、DTO、VO │ ├── entity/ # 数据库实体类(与表一一对应) │ ├── dto/ # 数据传输对象(层间传递) │ └── vo/ # 视图对象(返回给前端) └── sky-server/ # 后端服务模块(核心业务代码) ├── config/ # 配置类(Redis、Swagger、WebMvc等) ├── controller/ # 控制器层 ├── service/ # 业务逻辑层 ├── mapper/ # 数据访问层 ├── interceptor/ # 拦截器(如JWT认证拦截器) ├── aspect/ # AOP切面(公共字段填充) └── SkyApplication.java # 启动类POJO 分类说明:Entity:实体类,与数据库表一一对应DTO(Data Transfer Object):数据传输对象,用于层间传递数据VO(View Object):视图对象,专门返回给前端展示[reference:4]2.2 父工程 POM 配置(版本管理)<!-- sky-take-out/pom.xml --> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.0</version> <relativePath/> </parent> <groupId>com.sky</groupId> <artifactId>sky-take-out</artifactId> <version>1.0-SNAPSHOT</version> <packaging>pom</packaging> <properties> <java.version>1.8</java.version> <mybatis-plus.version>3.5.2</mybatis-plus.version> </properties> <dependencies> <!-- 统一管理的依赖在这里声明版本 --> </dependencies>2.3 子模块依赖配置<!-- sky-server/pom.xml --> <dependencies> <!-- Spring Boot Web Starter --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- MyBatis-Plus Starter --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.2</version> </dependency> <!-- MySQL 驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <!-- Redis --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!-- 引用公共模块 --> <dependency> <groupId>com.sky</groupId> <artifactId>sky-common</artifactId> <version>1.0-SNAPSHOT</version> </dependency> </dependencies>2.4 启动类package com.sky; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.transaction.annotation.EnableTransactionManagement; @Slf4j @SpringBootApplication @EnableTransactionManagement // 开启事务管理 public class SkyApplication { public static void main(String[] args) { SpringApplication.run(SkyApplication.class, args); log.info("苍穹外卖后端服务启动成功!"); } }@SpringBootApplication 复合注解解析:注解作用@SpringBootConfiguration标识为配置类(@Configuration 的派生)@ComponentScan开启组件扫描,扫描启动类所在包及子包@EnableAutoConfiguration开启自动配置(核心)自动配置原理(5 步必背):@SpringBootApplication → @EnableAutoConfiguration@Import(AutoConfigurationImportSelector.class) 导入选择器加载 META-INF/spring.factories(或 2.7 新路径 AutoConfiguration.imports)读取自动配置类的全限定类名通过 @ConditionalOnXxx 条件注解判断是否加载三、配置文件3.1 application.yml(苍穹外卖示例)server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/sky_take_out?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8 username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver redis: host: localhost port: 6379 database: 0 main: allow-circular-references: true # 允许循环依赖 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 打印 SQL 日志 global-config: db-config: id-type: auto # 主键自增 mapper-locations: classpath:mapper/*.xml sky: jwt: admin-secret-key: "itcast" admin-ttl: 7200000 admin-token-name: "token"3.2 多环境配置(必背)创建多个配置文件,通过 spring.profiles.active 激活:文件环境application.yml默认配置(公共)application-dev.yml开发环境application-prod.yml生产环境# application.yml(主配置) spring: profiles: active: dev # 激活开发环境 # application-dev.yml server: port: 8081 spring: config: activate: on-profile: dev # application-prod.yml server: port: 80 spring: config: activate: on-profile: prod激活方式:# 命令行激活 java -jar sky-server.jar --spring.profiles.active=prod # IDEA VM options -Dspring.profiles.active=prod四、分层架构(苍穹外卖实战)4.1 分层架构图解(外卖公司比喻)层级角色苍穹外卖示例Controller客服经理(接电话)接收 HTTP 请求,返回 JSON 响应Service业务主管(管流程)实现业务逻辑,调用 MapperMapper仓库管理员(管库存)执行 SQL,操作数据库Entity货物清单与数据库表一一对应的实体4.2 Entity 层(实体类)package com.sky.entity; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import java.io.Serializable; import java.time.LocalDateTime; @Data @Builder @NoArgsConstructor @AllArgsConstructor public class Employee implements Serializable { private static final long serialVersionUID = 1L; private Long id; private String username; private String name; private String password; private String phone; private String sex; private String idNumber; private Integer status; private LocalDateTime createTime; private LocalDateTime updateTime; private Long createUser; private Long updateUser; }4.3 Mapper 层(数据访问层)package com.sky.mapper; import com.sky.entity.Employee; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Select; @Mapper public interface EmployeeMapper { @Select("select * from employee where username = #{username}") Employee getByUsername(String username); // 分页查询员工 Page<Employee> pageQuery(EmployeePageQueryDTO pageQueryDTO); // 更新员工信息 void update(Employee employee); }MyBatis-Plus 增强版(推荐) :// 继承 BaseMapper,自动拥有 CRUD 方法 @Mapper public interface DishMapper extends BaseMapper<Dish> { // 无需写任何基础 CRUD,BaseMapper 已提供: // insert(), deleteById(), updateById(), selectById(), selectList() 等 } // Service 层还可以继承 IService @Service public class DishService extends ServiceImpl<DishMapper, Dish> { // 自动拥有 saveBatch(), updateBatchById() 等批量方法 }4.4 Service 层(业务逻辑层)package com.sky.service.impl; import com.sky.context.BaseContext; import com.sky.dto.EmployeeDTO; import com.sky.entity.Employee; import com.sky.mapper.EmployeeMapper; import com.sky.service.EmployeeService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.DigestUtils; @Slf4j @Service public class EmployeeServiceImpl implements EmployeeService { @Autowired private EmployeeMapper employeeMapper; @Override @Transactional // 开启事务 public void save(EmployeeDTO employeeDTO) { Employee employee = new Employee(); // 拷贝属性(DTO → Entity) BeanUtils.copyProperties(employeeDTO, employee); // 设置默认密码(MD5 加密) employee.setPassword(DigestUtils.md5DigestAsHex("123456".getBytes())); employee.setStatus(1); employee.setCreateTime(LocalDateTime.now()); employee.setUpdateTime(LocalDateTime.now()); // 从 ThreadLocal 获取当前操作用户 ID employee.setCreateUser(BaseContext.getCurrentId()); employee.setUpdateUser(BaseContext.getCurrentId()); employeeMapper.insert(employee); } }4.5 Controller 层(控制层)package com.sky.controller.admin; import com.sky.dto.EmployeeDTO; import com.sky.dto.EmployeeLoginDTO; import com.sky.dto.EmployeePageQueryDTO; import com.sky.result.PageResult; import com.sky.result.Result; import com.sky.service.EmployeeService; import com.sky.vo.EmployeeLoginVO; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/admin/employee") @Api(tags = "员工管理接口") @Slf4j public class EmployeeController { @Autowired private EmployeeService employeeService; @PostMapping("/login") @ApiOperation("员工登录") public Result<EmployeeLoginVO> login(@RequestBody EmployeeLoginDTO loginDTO) { log.info("员工登录:{}", loginDTO.getUsername()); EmployeeLoginVO employeeLoginVO = employeeService.login(loginDTO); return Result.success(employeeLoginVO); } @PostMapping @ApiOperation("新增员工") public Result save(@RequestBody EmployeeDTO employeeDTO) { log.info("新增员工:{}", employeeDTO); employeeService.save(employeeDTO); return Result.success(); } @GetMapping("/page") @ApiOperation("员工分页查询") public Result<PageResult> page(EmployeePageQueryDTO pageQueryDTO) { log.info("员工分页查询:{}", pageQueryDTO); PageResult pageResult = employeeService.pageQuery(pageQueryDTO); return Result.success(pageResult); } }4.6 分层调用链路总结HTTP 请求 ↓ Controller(接收参数,调用 Service) ↓ Service(业务逻辑,调用 Mapper) ↓ Mapper(执行 SQL,操作数据库) ↓ Entity(数据载体) ↓ 返回结果 → VO → JSON 响应⚠️ 常见错误:❌ Controller 直接调用 Mapper(越级)❌ Mapper 层写业务判断逻辑❌ Service 层写 SQL 拼接五、核心注解速查表5.1 组件注解注解用途层级@Component通用组件,交给 Spring 管理任意@ControllerWeb 控制器(配合 @ResponseBody 返回 JSON)表现层@RestController@Controller + @ResponseBody 的合并表现层@Service业务逻辑层组件业务层@Repository数据访问层组件,会转换持久层异常数据层@Repository 的特殊功能:将数据库操作抛出的异常自动转换为 Spring 统一的数据访问异常体系[reference:5]。5.2 请求映射注解注解作用@RequestMapping通用请求映射,可指定 method@GetMapping处理 GET 请求@PostMapping处理 POST 请求@PutMapping处理 PUT 请求@DeleteMapping处理 DELETE 请求5.3 参数绑定注解注解作用示例@PathVariable获取 URL 路径参数/user/{id} → @PathVariable Long id@RequestParam获取 URL 查询参数?name=张三 → @RequestParam String name@RequestBody获取请求体(JSON → 对象)@RequestBody EmployeeDTO dto@RequestHeader获取请求头@RequestHeader("token") String token5.4 依赖注入注解注解作用@Autowired按类型自动注入(Spring 原生)@Resource按名称自动注入(JSR-250)@Qualifier配合 @Autowired,指定 Bean 名称@Value注入配置文件中的值(如 ${server.port})@ConfigurationProperties批量注入配置属性5.5 配置与条件注解注解作用@Configuration标识配置类@Bean在配置类中声明 Bean@ConditionalOnClass类路径存在指定类时才生效@ConditionalOnMissingBean容器中没有指定 Bean 时才生效@ConditionalOnProperty配置文件存在指定属性时才生效六、全局异常处理6.1 苍穹外卖全局异常处理器package com.sky.handler; import com.sky.constant.MessageConstant; import com.sky.exception.BaseException; import com.sky.result.Result; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; @RestControllerAdvice @Slf4j public class GlobalExceptionHandler { // 捕获业务异常 @ExceptionHandler(BaseException.class) public Result exceptionHandler(BaseException ex) { log.error("业务异常:{}", ex.getMessage()); return Result.error(ex.getMessage()); } // 捕获 SQL 异常(如用户名重复) @ExceptionHandler(DuplicateKeyException.class) public Result handleDuplicateKeyException(DuplicateKeyException ex) { log.error("SQL 异常:{}", ex.getMessage()); return Result.error(MessageConstant.ALREADY_EXISTS); } // 兜底异常处理 @ExceptionHandler(Exception.class) public Result exceptionHandler(Exception ex) { log.error("系统异常:{}", ex.getMessage()); return Result.error(MessageConstant.UNKNOWN_ERROR); } }6.2 统一响应对象package com.sky.result; import lombok.Data; import java.io.Serializable; @Data public class Result<T> implements Serializable { private Integer code; // 1 成功,0 失败 private String msg; private T data; public static <T> Result<T> success() { Result<T> result = new Result<>(); result.code = 1; return result; } public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.code = 1; result.data = data; return result; } public static <T> Result<T> error(String msg) { Result<T> result = new Result<>(); result.code = 0; result.msg = msg; return result; } }七、JWT 认证与拦截器7.1 JWT 工具类package com.sky.utils; import io.jsonwebtoken.*; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; import java.util.Date; import java.util.Map; @Data @Component @ConfigurationProperties(prefix = "sky.jwt") public class JwtUtil { private String adminSecretKey; private Long adminTtl; private String adminTokenName; // 生成 JWT 令牌 public String createJWT(Map<String, Object> claims, Long ttlMillis) { long nowMillis = System.currentTimeMillis(); Date now = new Date(nowMillis); JwtBuilder builder = Jwts.builder() .setClaims(claims) .setIssuedAt(now) .signWith(SignatureAlgorithm.HS256, adminSecretKey); if (ttlMillis > 0) { builder.setExpiration(new Date(nowMillis + ttlMillis)); } return builder.compact(); } // 解析 JWT 令牌 public Claims parseJWT(String token) { return Jwts.parser() .setSigningKey(adminSecretKey) .parseClaimsJws(token) .getBody(); } }7.2 JWT 认证拦截器package com.sky.interceptor; import com.sky.constant.JwtClaimsConstant; import com.sky.context.BaseContext; import com.sky.properties.JwtProperties; import com.sky.utils.JwtUtil; import io.jsonwebtoken.Claims; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @Component @Slf4j public class JwtTokenAdminInterceptor implements HandlerInterceptor { @Autowired private JwtUtil jwtUtil; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { // 判断当前拦截的是否为 Controller 方法 if (!(handler instanceof HandlerMethod)) { return true; } // 从请求头中获取令牌 String token = request.getHeader(jwtUtil.getAdminTokenName()); // 解析令牌 try { Claims claims = jwtUtil.parseJWT(token); Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString()); // 将员工 ID 存入 ThreadLocal(供后续业务使用) BaseContext.setCurrentId(empId); log.info("当前员工 ID:{}", empId); return true; } catch (Exception ex) { log.error("JWT 解析失败:{}", ex.getMessage()); response.setStatus(401); return false; } } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { // 请求结束后清除 ThreadLocal,防止内存泄漏 BaseContext.removeCurrentId(); } }7.3 ThreadLocal 工具类(苍穹外卖)package com.sky.context; public class BaseContext { private static ThreadLocal<Long> threadLocal = new ThreadLocal<>(); public static void setCurrentId(Long id) { threadLocal.set(id); } public static Long getCurrentId() { return threadLocal.get(); } public static void removeCurrentId() { threadLocal.remove(); } }ThreadLocal 原理:每个线程拥有自己的变量副本,线程隔离。用完必须 remove(),否则在线程池环境下会导致内存泄漏[reference:6]。7.4 注册拦截器package com.sky.config; import com.sky.interceptor.JwtTokenAdminInterceptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport; @Configuration public class WebMvcConfig extends WebMvcConfigurationSupport { @Autowired private JwtTokenAdminInterceptor jwtTokenAdminInterceptor; @Override protected void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(jwtTokenAdminInterceptor) .addPathPatterns("/admin/**") // 拦截所有 /admin 开头的请求 .excludePathPatterns("/admin/employee/login") // 排除登录接口 .excludePathPatterns("/admin/employee/logout"); } }八、AOP 切面编程(公共字段自动填充)8.1 自定义注解package com.sky.annotation; import com.sky.enumeration.OperationType; import java.lang.annotation.*; @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface AutoFill { OperationType value(); // UPDATE 或 INSERT }8.2 切面类实现package com.sky.aspect; import com.sky.annotation.AutoFill; import com.sky.constant.AutoFillConstant; import com.sky.context.BaseContext; import com.sky.enumeration.OperationType; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.stereotype.Component; import java.lang.reflect.Method; import java.time.LocalDateTime; @Aspect @Component @Slf4j public class AutoFillAspect { // 切点:标注了 @AutoFill 注解的方法 @Pointcut("@annotation(com.sky.annotation.AutoFill)") public void autoFillPointCut() {} @Before("autoFillPointCut()") public void autoFill(JoinPoint joinPoint) { // 获取当前被拦截的方法上的注解 MethodSignature signature = (MethodSignature) joinPoint.getSignature(); AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class); OperationType operationType = autoFill.value(); // 获取方法的参数(实体对象) Object[] args = joinPoint.getArgs(); if (args == null || args.length == 0) return; Object entity = args[0]; // 准备要赋的值 LocalDateTime now = LocalDateTime.now(); Long currentId = BaseContext.getCurrentId(); // 根据操作类型设置不同字段 if (operationType == OperationType.INSERT) { // 插入操作:设置 createTime, updateTime, createUser, updateUser try { Method setCreateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class); Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class); Method setCreateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class); Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class); setCreateTime.invoke(entity, now); setUpdateTime.invoke(entity, now); setCreateUser.invoke(entity, currentId); setUpdateUser.invoke(entity, currentId); } catch (Exception e) { log.error("公共字段填充失败:{}", e.getMessage()); } } else if (operationType == OperationType.UPDATE) { // 更新操作:设置 updateTime, updateUser try { Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class); Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class); setUpdateTime.invoke(entity, now); setUpdateUser.invoke(entity, currentId); } catch (Exception e) { log.error("公共字段填充失败:{}", e.getMessage()); } } } }8.3 使用示例@Mapper public interface EmployeeMapper { @AutoFill(OperationType.INSERT) void insert(Employee employee); @AutoFill(OperationType.UPDATE) void update(Employee employee); }九、缓存与 Redis9.1 Spring Cache 配置package com.sky.config; import org.springframework.cache.annotation.EnableCaching; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.cache.RedisCacheConfiguration; import org.springframework.data.redis.serializer.RedisSerializationContext; import org.springframework.data.redis.serializer.StringRedisSerializer; import java.time.Duration; @Configuration @EnableCaching public class RedisConfig { @Bean public RedisCacheConfiguration cacheConfiguration() { return RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofMinutes(30)) // 缓存过期时间 30 分钟 .disableCachingNullValues() // 不缓存 null 值 .serializeKeysWith(RedisSerializationContext.SerializationPair .fromSerializer(new StringRedisSerializer())); } }9.2 缓存注解使用@Service @Slf4j public class DishServiceImpl implements DishService { @Autowired private DishMapper dishMapper; // 查询时缓存:key = "dish:categoryId:" + categoryId @Cacheable(value = "dish", key = "'dish:categoryId:' + #categoryId") public List<Dish> getByCategoryId(Long categoryId) { return dishMapper.getByCategoryId(categoryId); } // 更新时清空缓存 @CacheEvict(value = "dish", key = "'dish:categoryId:' + #dishDTO.categoryId") public void updateWithFlavor(DishDTO dishDTO) { // 更新业务逻辑 } // 批量清空缓存(key 模糊匹配) @CacheEvict(value = "dish", allEntries = true) public void deleteBatch(List<Long> ids) { // 删除业务逻辑 } }9.3 常用 Redis 操作(StringRedisTemplate)@Service public class ShopStatusService { @Autowired private StringRedisTemplate redisTemplate; private static final String SHOP_STATUS_KEY = "SHOP_STATUS"; public void setStatus(Integer status) { redisTemplate.opsForValue().set(SHOP_STATUS_KEY, String.valueOf(status)); } public Integer getStatus() { String status = redisTemplate.opsForValue().get(SHOP_STATUS_KEY); return status == null ? 0 : Integer.parseInt(status); } }十、Spring Task 定时任务10.1 开启定时任务@SpringBootApplication @EnableScheduling // 开启定时任务 public class SkyApplication { public static void main(String[] args) { SpringApplication.run(SkyApplication.class, args); } }10.2 订单状态定时处理package com.sky.task; import com.sky.service.OrderService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; @Component @Slf4j public class OrderTask { @Autowired private OrderService orderService; // 每天凌晨 1 点处理超时未支付的订单 @Scheduled(cron = "0 0 1 * * ?") public void processTimeoutOrder() { log.info("定时任务:处理超时未支付订单"); orderService.processTimeoutOrders(); } // 每分钟执行一次,处理配送中的订单(送达后自动完成) @Scheduled(cron = "0 * * * * ?") public void processDeliveryOrder() { log.info("定时任务:处理配送中订单"); orderService.processDeliveryOrders(); } }10.3 Cron 表达式速查字段允许值特殊字符秒0-59, - * /分0-59, - * /时0-23, - * /日1-31, - * ? / L W月1-12, - * /周0-7(0 或 7 都代表周日), - * ? / L #年(可选)空或 1970-2099, - * /常用表达式:0 0 1 * * ? → 每天凌晨 1 点0 */5 * * * ? → 每 5 分钟一次0 0 12 * * MON-FRI → 工作日中午 12 点十一、WebSocket 实时通信11.1 WebSocket 配置package com.sky.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.socket.server.standard.ServerEndpointExporter; @Configuration public class WebSocketConfig { @Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } }11.2 WebSocket 服务端package com.sky.websocket; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import javax.websocket.*; import javax.websocket.server.PathParam; import javax.websocket.server.ServerEndpoint; import java.util.concurrent.ConcurrentHashMap; @Component @ServerEndpoint("/ws/{sid}") @Slf4j public class WebSocketServer { // 存储所有在线会话(线程安全) private static ConcurrentHashMap<String, Session> sessionMap = new ConcurrentHashMap<>(); @OnOpen public void onOpen(Session session, @PathParam("sid") String sid) { sessionMap.put(sid, session); log.info("WebSocket 连接建立:{}", sid); } @OnClose public void onClose(@PathParam("sid") String sid) { sessionMap.remove(sid); log.info("WebSocket 连接关闭:{}", sid); } @OnMessage public void onMessage(String message, Session session) { log.info("收到消息:{}", message); } @OnError public void onError(Session session, Throwable error) { log.error("WebSocket 错误:{}", error.getMessage()); } // 发送消息给指定用户 public void sendToClient(String sid, String message) { Session session = sessionMap.get(sid); if (session != null && session.isOpen()) { try { session.getBasicRemote().sendText(message); log.info("发送消息给 {}:{}", sid, message); } catch (Exception e) { log.error("发送失败:{}", e.getMessage()); } } } }11.3 来单提醒/催单场景@Service public class OrderServiceImpl implements OrderService { @Autowired private WebSocketServer webSocketServer; // 用户下单 → 来单提醒(推送给管理端) public void submit(OrdersSubmitDTO submitDTO) { // ... 下单业务逻辑 // 来单提醒 Map<String, Object> reminder = new HashMap<>(); reminder.put("type", 1); // 1=来单提醒 reminder.put("orderId", order.getId()); reminder.put("content", "您有一笔新订单"); webSocketServer.sendToClient("admin", JSON.toJSONString(reminder)); } // 用户催单 → 推送给管理端 public void reminder(Long orderId) { Map<String, Object> reminder = new HashMap<>(); reminder.put("type", 2); // 2=用户催单 reminder.put("orderId", orderId); reminder.put("content", "用户催单啦"); webSocketServer.sendToClient("admin", JSON.toJSONString(reminder)); } }十二、文件上传(阿里云 OSS)12.1 OSS 配置属性类package com.sky.properties; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; @Data @Component @ConfigurationProperties(prefix = "sky.alioss") public class AliOssProperties { private String endpoint; private String accessKeyId; private String accessKeySecret; private String bucketName; }12.2 OSS 工具类package com.sky.utils; import com.aliyun.oss.OSS; import com.aliyun.oss.OSSClientBuilder; import com.sky.properties.AliOssProperties; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; import java.util.UUID; @Component @Slf4j public class AliOssUtil { @Autowired private AliOssProperties aliOssProperties; public String upload(MultipartFile file) { String endpoint = aliOssProperties.getEndpoint(); String accessKeyId = aliOssProperties.getAccessKeyId(); String accessKeySecret = aliOssProperties.getAccessKeySecret(); String bucketName = aliOssProperties.getBucketName(); // 生成唯一文件名(防止覆盖) String originalFilename = file.getOriginalFilename(); String extension = originalFilename.substring(originalFilename.lastIndexOf(".")); String objectName = UUID.randomUUID().toString() + extension; OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret); try { ossClient.putObject(bucketName, objectName, file.getInputStream()); // 返回文件访问 URL return "https://" + bucketName + "." + endpoint + "/" + objectName; } catch (IOException e) { log.error("文件上传失败:{}", e.getMessage()); throw new RuntimeException(e); } finally { ossClient.shutdown(); } } }12.3 通用上传接口@RestController @RequestMapping("/admin/common") @Api(tags = "通用接口") @Slf4j public class CommonController { @Autowired private AliOssUtil aliOssUtil; @PostMapping("/upload") @ApiOperation("文件上传") public Result<String> upload(MultipartFile file) { log.info("文件上传:{}", file.getOriginalFilename()); String url = aliOssUtil.upload(file); return Result.success(url); } }十三、Knife4j + Swagger 接口文档13.1 添加依赖<dependency> <groupId>com.github.xiaoymin</groupId> <artifactId>knife4j-spring-boot-starter</artifactId> <version>3.0.3</version> </dependency>13.2 Swagger 配置类package com.sky.config; import com.github.xiaoymin.knife4j.spring.annotations.EnableKnife4j; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import springfox.documentation.builders.ApiInfoBuilder; import springfox.documentation.builders.PathSelectors; import springfox.documentation.builders.RequestHandlerSelectors; import springfox.documentation.service.ApiInfo; import springfox.documentation.spi.DocumentationType; import springfox.documentation.spring.web.plugins.Docket; import springfox.documentation.swagger2.annotations.EnableSwagger2; @Configuration @EnableSwagger2 @EnableKnife4j public class SwaggerConfig { @Bean public Docket adminApi() { return new Docket(DocumentationType.SWAGGER_2) .groupName("管理端接口") .apiInfo(apiInfo()) .select() .apis(RequestHandlerSelectors.basePackage("com.sky.controller.admin")) .paths(PathSelectors.any()) .build(); } @Bean public Docket userApi() { return new Docket(DocumentationType.SWAGGER_2) .groupName("用户端接口") .apiInfo(apiInfo()) .select() .apis(RequestHandlerSelectors.basePackage("com.sky.controller.user")) .paths(PathSelectors.any()) .build(); } private ApiInfo apiInfo() { return new ApiInfoBuilder() .title("苍穹外卖 API 文档") .version("1.0") .description("苍穹外卖后端接口文档") .build(); } }13.3 使用示例@Api(tags = "员工管理接口") @RestController @RequestMapping("/admin/employee") public class EmployeeController { @PostMapping("/login") @ApiOperation("员工登录") @ApiImplicitParams({ @ApiImplicitParam(name = "username", value = "用户名", required = true), @ApiImplicitParam(name = "password", value = "密码", required = true) }) public Result<EmployeeLoginVO> login(@RequestBody EmployeeLoginDTO loginDTO) { // ... } }访问地址:http://localhost:8080/doc.html十四、常用工具类速查14.1 BeanUtils(属性拷贝)// DTO → Entity Employee employee = new Employee(); BeanUtils.copyProperties(employeeDTO, employee);14.2 DigestUtils(MD5 加密)// 密码 MD5 加密 String encryptedPassword = DigestUtils.md5DigestAsHex("123456".getBytes());14.3 Jackson(JSON 序列化)// 对象 → JSON 字符串 String jsonStr = JSON.toJSONString(object); // JSON 字符串 → 对象 User user = JSON.parseObject(jsonStr, User.class);14.4 LocalDateTime(时间处理)// 获取当前时间 LocalDateTime now = LocalDateTime.now(); // 时间格式化 DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); String formatted = now.format(formatter);十五、项目部署与运行15.1 打包# 在项目根目录执行 mvn clean package # 跳过测试打包 mvn clean package -DskipTests15.2 运行# 运行 JAR 包 java -jar sky-server.jar # 指定环境运行 java -jar sky-server.jar --spring.profiles.active=prod # 后台运行 nohup java -jar sky-server.jar > app.log 2>&1 &15.3 Nginx 反向代理配置server { listen 80; server_name api.sky.com; location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }十六、快速记忆口诀启动流程SpringApplication.run → 初始化上下文 → 加载自动配置 → 启动内嵌容器 → 注册 Controller → 应用就绪自动配置@SpringBootApplication → @EnableAutoConfiguration → @Import → spring.factories → @ConditionalOnXxx分层架构Controller 接请求,Service 写逻辑,Mapper 查数据库,Entity 存数据JWT 认证登录签发 Token → 请求携带 Header → 拦截器校验 → ThreadLocal 存用户 ID → 业务使用 → 请求结束清除缓存注解@Cacheable 查缓存,@CachePut 更新缓存,@CacheEvict 清缓存定时任务@EnableScheduling 开启,@Scheduled(cron) 标记,Cron 表达式定时间全局异常@RestControllerAdvice 全局处理,@ExceptionHandler 分类捕获本笔记基于 Spring Boot 2.7 + 苍穹外卖实战项目整理,覆盖了从项目搭建到部署上线的完整流程,适合日常开发查阅和面试前背诵。建议结合源码实践加深理解。
2026年04月03日
23 阅读
0 评论
0 点赞
1
2
3
4