《如何写好一个状态机》—— 从入门到放弃系列
前言以前在做支付系统的入金模块时，遇到了一个经典问题：交易状态流转。你以为状态机就是 if-else 一把梭？年轻人，图样图森破。当你的状态从 3 个变成 10 个，事件从 5 个变成 20 个，你就会发现自己写的代码比《百年孤独》里的家谱还难理清。今天，我们就来聊聊如何优雅地实现一个状态机，让你的代码不至于成为同事口中的"祖传屎山"。什么是状态机？概念定义状态机（State Machine），全称是有限状态机（Finite State Machine，简称 FSM），是一种数学计算模型。它由以下几个核心要素组成：graph LR subgraph 状态机核心要素 S[状态 State] --> E[事件 Event] E --> T[转换 Transition] T --> A[动作 Action] A --> S end要素说明支付场景示例状态（State）系统在某一时刻的状况待验证、处理中、成功、失败事件（Event）触发状态变化的外部输入用户提交、审核通过、支付回调转换（Transition）从一个状态到另一个状态的变化待验证 → 已验证动作（Action）状态转换时执行的操作发送通知、更新余额、记录日志守卫（Guard）转换前的条件检查余额是否充足、账户是否激活用人话说：状态机就是一套规则，定义了"在什么状态下，发生什么事件，会转换到什么状态，并且执行什么动作"。为什么状态机如此重要？(尤其对于支付系统)1. 业务逻辑的"契约化"没有状态机的代码通常长这样：// 💀 地狱级代码示例 public void processPayment(Payment payment, String action) { if (payment.getStatus().equals("PENDING") && action.equals("approve")) { if (payment.getAmount() > 10000) { if (payment.getUserLevel().equals("VIP")) { payment.setStatus("APPROVED"); } else { payment.setStatus("NEED_REVIEW"); } } else { payment.setStatus("APPROVED"); } } else if (payment.getStatus().equals("APPROVED") && action.equals("pay")) { // 又是一堆 if-else... } else if (...) { // 无限嵌套... } }这种代码的问题在于：状态流转规则散落在各处，没有人能说清楚系统到底有多少种状态、多少种流转路径。而有了状态机，业务规则变成了"契约"：状态A + 事件X → 状态B（必须满足条件C）新人接手项目，看一眼状态流转图就能理解业务；产品经理提需求，先画状态图再讨论；出了 bug，对照状态机排查一目了然。2. 防止"非法状态转换"在支付系统中，状态的正确性直接关系到钱。想象一下这些恐怖场景：一笔已经退款成功的交易，又被标记为支付成功（用户白嫖）一笔处理中的交易，直接跳到已完成（跳过了风控检查）一笔已取消的订单，又被重新激活（幽灵订单）状态机通过白名单机制杜绝这些问题：只有明确定义的流转路径才是合法的，其他一律拒绝。// 状态机会直接拒绝非法操作 stateMachine.fire(DepositEvent.DEPOSIT_SUCCESS); // IllegalStateException: 状态[REFUND_SUCCESS]不支持事件[DEPOSIT_SUCCESS]3. 天然的审计追踪能力金融系统对审计的要求非常高。有了状态机，每一次状态变更都是一个明确的"事件"：2024-01-15 10:23:45 | TXN_001 | WAIT_VERIFY → VERIFIED | EVENT: VERIFY_PASS | OPERATOR: SYSTEM 2024-01-15 10:23:46 | TXN_001 | VERIFIED → PENDING | EVENT: DEPOSIT_PENDING | OPERATOR: SYSTEM 2024-01-15 10:25:12 | TXN_001 | PENDING → REFUND_PROCESSING | EVENT: DEPOSIT_FAILED | OPERATOR: SYSTEM 2024-01-15 14:30:00 | TXN_001 | REFUND_PROCESSING → REFUND_SUCCESS | EVENT: REFUND_SUCCESS | OPERATOR: SYSTEM完整的状态流转链路，出了问题可以精确定位到是哪个环节、什么时间、由谁触发的。4. 支持复杂的业务编排实际的支付系统远比想象的复杂：并行状态：一笔交易可能同时处于"支付处理中"和"风控审核中"嵌套状态：退款流程本身也是一个子状态机超时处理：处理中状态超过 30 分钟自动触发人工审核补偿机制：某个步骤失败后自动触发回滚这些复杂场景，用 if-else 写出来会是什么样子？我不敢想。而状态机天然支持这些高级特性。5. 代码即文档一个设计良好的状态机，本身就是最好的业务文档：// 看这段代码，不需要任何注释，你就知道业务规则是什么 .transition() .from(DepositStatus.PENDING) .to(DepositStatus.REFUND_PROCESSING) .on(DepositEvent.DEPOSIT_FAILED) .guard(ctx -> ctx.getTransaction().getRetryCount() >= 3) // 重试3次才退款 .action(ctx -> refundService.initiateRefund(ctx.getPayload()))状态机在支付系统中的典型应用支付系统可以说是状态机的"最佳实践场"，几乎每个核心流程都离不开它：graph TB subgraph 支付系统状态机应用 A[订单状态机] --> A1[待支付→支付中→已支付→已完成] B[支付状态机] --> B1[创建→处理中→成功/失败] C[退款状态机] --> C1[申请→审核→处理→完成] D[对账状态机] --> D1[待对账→对账中→已平账/差异] E[结算状态机] --> E1[待结算→结算中→已结算] end在我们的入金场景中，一笔交易从收到客户的转账，到最终入账成功或退款完成，要经历多个状态的流转。每个状态代表着交易在某个特定阶段，每次状态变更都意味着业务的推进。好了，理论知识铺垫完毕。接下来，让我们看看这个真实的入金状态流转图，然后用五种不同的方式来实现它。问题场景先看一下我们入金系统的状态流转图：stateDiagram-v2 [*] --> WV: 正常入金 [*] --> MC: IBAN未找到 WV --> V: 验证通过 WV --> MC: 交易验证不通过 V --> P: 入金处理中 V --> RP: 账户状态校验失败 P --> P: 仍在处理中 P --> S: 入金成功 P --> RP: 入金失败 MC --> RP: 人工确认退款 MC --> RS: 人工确认退款成功 MC --> MF: 人工确认失败 RP --> RS: 退款成功 RP --> RF: 退款失败 RF --> RS: 人工确认退款成功 RS --> RT: 收到退票通知 RS --> [*]: 流程结束 RT --> RS: 人工确认退款成功 MF --> [*]: 流程结束 S --> [*]: 流程结束 note right of MC: 🧑‍💼 需人工处理 note right of RF: 🧑‍💼 需人工处理 note right of RT: 🧑‍💼 需人工处理状态说明：状态缩写全称说明处理方式WVWait_Verify等待验证系统自动VVerified已验证系统自动PPending处理中系统自动SSuccess成功系统自动MCManual_Confirm人工确认人工处理RPRefund_Processing退款处理中系统自动RSRefund_Success退款成功系统自动RFRefund_Failed退款失败人工处理MFManual_Failed人工确认失败系统自动RTReturn_Ticket退票人工处理看到这张图，是不是有种想转行的冲动？别慌，我们一步步来拆解。方案一：Switch/If-Else 硬刚（适合勇士）这是最原始、最直接、最容易被后人骂的方式。但作为入门，还是要了解一下。状态与事件定义/** * 入金交易状态枚举 */ public enum DepositStatus { WAIT_VERIFY("WV", "等待验证"), VERIFIED("V", "已验证"), PENDING("P", "处理中"), SUCCESS("S", "成功"), MANUAL_CONFIRM("MC", "人工确认"), REFUND_PROCESSING("RP", "退款处理中"), REFUND_SUCCESS("RS", "退款成功"), REFUND_FAILED("RF", "退款失败"), MANUAL_FAILED("MF", "人工确认失败"), RETURN_TICKET("RT", "退票"); private final String code; private final String desc; DepositStatus(String code, String desc) { this.code = code; this.desc = desc; } public String getCode() { return code; } public String getDesc() { return desc; } } /** * 状态流转事件枚举 */ public enum DepositEvent { IBAN_NOT_FOUND("IBAN未找到"), VERIFY_PASS("验证通过"), VERIFY_FAIL("验证失败"), DEPOSIT_PENDING("入金处理中"), DEPOSIT_SUCCESS("入金成功"), DEPOSIT_FAILED("入金失败"), ACCOUNT_VALIDATE_FAIL("账户校验失败"), MANUAL_CONFIRM_REFUND("人工确认退款"), MANUAL_CONFIRM_SUCCESS("人工确认成功"), MANUAL_CONFIRM_FAILED("人工确认失败"), REFUND_SUCCESS("退款成功"), REFUND_FAILED("退款失败"), RECEIVE_RETURN_TICKET("收到退票通知"), STILL_PENDING("仍在处理中"); private final String desc; DepositEvent(String desc) { this.desc = desc; } public String getDesc() { return desc; } }Switch 实现/** * 状态机服务 - Switch版本 * * 优点：简单直接，一眼能看懂 * 缺点：当状态和事件增多时，这个方法会膨胀到让你怀疑人生 */ @Service @Slf4j public class DepositStateMachineSwitch { public DepositStatus transition(DepositStatus currentStatus, DepositEvent event) { log.info("状态流转: {} + {} -> ?", currentStatus, event); switch (currentStatus) { case WAIT_VERIFY: return handleWaitVerify(event); case VERIFIED: return handleVerified(event); case PENDING: return handlePending(event); case MANUAL_CONFIRM: return handleManualConfirm(event); case REFUND_PROCESSING: return handleRefundProcessing(event); case REFUND_FAILED: return handleRefundFailed(event); case REFUND_SUCCESS: return handleRefundSuccess(event); case RETURN_TICKET: return handleReturnTicket(event); case SUCCESS: case MANUAL_FAILED: // 终态，不再流转 throw new IllegalStateException("终态不允许继续流转: " + currentStatus); default: throw new IllegalArgumentException("未知状态: " + currentStatus); } } private DepositStatus handleWaitVerify(DepositEvent event) { switch (event) { case VERIFY_PASS: return DepositStatus.VERIFIED; case VERIFY_FAIL: return DepositStatus.MANUAL_CONFIRM; default: throw new IllegalStateException( String.format("状态[%s]不支持事件[%s]", DepositStatus.WAIT_VERIFY, event)); } } private DepositStatus handleVerified(DepositEvent event) { switch (event) { case DEPOSIT_PENDING: return DepositStatus.PENDING; case ACCOUNT_VALIDATE_FAIL: return DepositStatus.REFUND_PROCESSING; default: throw new IllegalStateException( String.format("状态[%s]不支持事件[%s]", DepositStatus.VERIFIED, event)); } } private DepositStatus handlePending(DepositEvent event) { switch (event) { case STILL_PENDING: return DepositStatus.PENDING; // 自循环 case DEPOSIT_SUCCESS: return DepositStatus.SUCCESS; case DEPOSIT_FAILED: return DepositStatus.REFUND_PROCESSING; default: throw new IllegalStateException( String.format("状态[%s]不支持事件[%s]", DepositStatus.PENDING, event)); } } private DepositStatus handleManualConfirm(DepositEvent event) { switch (event) { case MANUAL_CONFIRM_REFUND: return DepositStatus.REFUND_PROCESSING; case MANUAL_CONFIRM_SUCCESS: return DepositStatus.REFUND_SUCCESS; case MANUAL_CONFIRM_FAILED: return DepositStatus.MANUAL_FAILED; default: throw new IllegalStateException( String.format("状态[%s]不支持事件[%s]", DepositStatus.MANUAL_CONFIRM, event)); } } private DepositStatus handleRefundProcessing(DepositEvent event) { switch (event) { case REFUND_SUCCESS: return DepositStatus.REFUND_SUCCESS; case REFUND_FAILED: return DepositStatus.REFUND_FAILED; default: throw new IllegalStateException( String.format("状态[%s]不支持事件[%s]", DepositStatus.REFUND_PROCESSING, event)); } } private DepositStatus handleRefundFailed(DepositEvent event) { if (event == DepositEvent.MANUAL_CONFIRM_SUCCESS) { return DepositStatus.REFUND_SUCCESS; } throw new IllegalStateException( String.format("状态[%s]不支持事件[%s]", DepositStatus.REFUND_FAILED, event)); } private DepositStatus handleRefundSuccess(DepositEvent event) { if (event == DepositEvent.RECEIVE_RETURN_TICKET) { return DepositStatus.RETURN_TICKET; } throw new IllegalStateException( String.format("状态[%s]不支持事件[%s]", DepositStatus.REFUND_SUCCESS, event)); } private DepositStatus handleReturnTicket(DepositEvent event) { if (event == DepositEvent.MANUAL_CONFIRM_SUCCESS) { return DepositStatus.REFUND_SUCCESS; } throw new IllegalStateException( String.format("状态[%s]不支持事件[%s]", DepositStatus.RETURN_TICKET, event)); } }点评： 这种写法就像你家的电线，刚装修完还能看，过两年再看就是一团乱麻。不过它胜在简单，适合状态少、改动少的场景。方案二：状态模式（设计模式爱好者的最爱）状态模式是 GoF 23 种设计模式之一，核心思想是把每个状态的行为封装到独立的类中。类图设计classDiagram class DepositState["«interface» DepositState"] { +handle(context, event) +getStatus() } class DepositContext { -currentState -transaction +fireEvent(event) +setState(state) +getCurrentStatus() } class WaitVerifyState { +handle(context, event) +getStatus() } class VerifiedState { +handle(context, event) +getStatus() } class PendingState { +handle(context, event) +getStatus() } class ManualConfirmState { +handle(context, event) +getStatus() } DepositState TransitionContext核心代码/** * 流转上下文 */ @Data @Builder public class TransitionContext<S, E> { private final S source; private final S target; private final E event; private final Object payload; private final Map<String, Object> variables; @SuppressWarnings("unchecked") public <T> T getPayload() { return (T) payload; } public <T> T getVariable(String key, Class<T> type) { return type.cast(variables.get(key)); } } /** * 状态流转定义 */ @Data @Builder public class Transition<S, E> { private final S source; private final S target; private final E event; private final Predicate<TransitionContext<S, E>> guard; private final Consumer<TransitionContext<S, E>> action; /** * 执行流转 */ public boolean execute(TransitionContext<S, E> context) { // 检查守卫条件 if (guard != null && !guard.test(context)) { return false; } // 执行动作 if (action != null) { action.accept(context); } return true; } } /** * 状态变更监听器 */ @FunctionalInterface public interface StateChangeListener<S, E> { void onStateChanged(S from, S to, E event, Object payload); } /** * 轻量级状态机 */ @Slf4j public class LiteStateMachine<S, E> { private volatile S currentState; private final Map<S, Map<E, Transition<S, E>>> transitionTable; private final Set finalStates; private final List<StateChangeListener<S, E>> listeners; private final Map<String, Object> variables; private final ReentrantLock lock = new ReentrantLock(); private LiteStateMachine(Builder<S, E> builder) { this.currentState = builder.initialState; this.transitionTable = builder.transitionTable; this.finalStates = builder.finalStates; this.listeners = new CopyOnWriteArrayList<>(builder.listeners); this.variables = new ConcurrentHashMap<>(builder.variables); } /** * 触发事件 */ public boolean fire(E event, Object payload) { lock.lock(); try { if (isFinalState()) { log.warn("当前状态[{}]是终态，无法触发事件[{}]", currentState, event); return false; } Map<E, Transition<S, E>> transitions = transitionTable.get(currentState); if (transitions == null) { log.warn("状态[{}]没有配置任何流转规则", currentState); return false; } Transition<S, E> transition = transitions.get(event); if (transition == null) { log.warn("状态[{}]不支持事件[{}]", currentState, event); return false; } TransitionContext<S, E> context = TransitionContext.<S, E>builder() .source(currentState) .target(transition.getTarget()) .event(event) .payload(payload) .variables(new HashMap<>(variables)) .build(); // 执行流转 if (!transition.execute(context)) { log.info("流转被Guard拦截: {} --[{}]--> {}", currentState, event, transition.getTarget()); return false; } S oldState = currentState; currentState = transition.getTarget(); log.info("状态流转成功: {} --[{}]--> {}", oldState, event, currentState); // 通知监听器 listeners.forEach(l -> { try { l.onStateChanged(oldState, currentState, event, payload); } catch (Exception e) { log.error("监听器执行异常", e); } }); return true; } finally { lock.unlock(); } } public boolean fire(E event) { return fire(event, null); } public S getCurrentState() { return currentState; } public boolean canFire(E event) { Map<E, Transition<S, E>> transitions = transitionTable.get(currentState); return transitions != null && transitions.containsKey(event); } public Set<E> getAvailableEvents() { Map<E, Transition<S, E>> transitions = transitionTable.get(currentState); return transitions != null ? transitions.keySet() : Collections.emptySet(); } public boolean isFinalState() { return finalStates.contains(currentState); } public void setVariable(String key, Object value) { variables.put(key, value); } public void addListener(StateChangeListener<S, E> listener) { listeners.add(listener); } /** * 创建构建器 */ public static <S, E> Builder<S, E> builder() { return new Builder<>(); } /** * 状态机构建器 */ public static class Builder<S, E> { private S initialState; private final Map<S, Map<E, Transition<S, E>>> transitionTable = new HashMap<>(); private final Set finalStates = new HashSet<>(); private final List<StateChangeListener<S, E>> listeners = new ArrayList<>(); private final Map<String, Object> variables = new HashMap<>(); public Builder<S, E> initialState(S state) { this.initialState = state; return this; } public Builder<S, E> finalStates(S... states) { this.finalStates.addAll(Arrays.asList(states)); return this; } public TransitionBuilder transition() { return new TransitionBuilder(); } public Builder<S, E> listener(StateChangeListener<S, E> listener) { this.listeners.add(listener); return this; } public Builder<S, E> variable(String key, Object value) { this.variables.put(key, value); return this; } public LiteStateMachine<S, E> build() { Objects.requireNonNull(initialState, "初始状态不能为空"); return new LiteStateMachine<>(this); } /** * 流转构建器 */ public class TransitionBuilder { private S source; private S target; private E event; private Predicate<TransitionContext<S, E>> guard; private Consumer<TransitionContext<S, E>> action; public TransitionBuilder from(S source) { this.source = source; return this; } public TransitionBuilder to(S target) { this.target = target; return this; } public TransitionBuilder on(E event) { this.event = event; return this; } public TransitionBuilder guard(Predicate<TransitionContext<S, E>> guard) { this.guard = guard; return this; } public TransitionBuilder action(Consumer<TransitionContext<S, E>> action) { this.action = action; return this; } public Builder<S, E> and() { Transition<S, E> transition = Transition.<S, E>builder() .source(source) .target(target) .event(event) .guard(guard) .action(action) .build(); transitionTable .computeIfAbsent(source, k -> new HashMap<>()) .put(event, transition); return Builder.this; } } } }使用示例/** * 入金状态机工厂 */ @Component @Slf4j public class DepositStateMachineFactory { private final RefundService refundService; private final NotificationService notificationService; public DepositStateMachineFactory(RefundService refundService, NotificationService notificationService) { this.refundService = refundService; this.notificationService = notificationService; } /** * 创建入金状态机实例 */ public LiteStateMachine<DepositStatus, DepositEvent> create(DepositTransaction transaction) { return LiteStateMachine.<DepositStatus, DepositEvent>builder() // 根据交易当前状态初始化 .initialState(transaction.getStatus()) // 定义终态 .finalStates(DepositStatus.SUCCESS, DepositStatus.MANUAL_FAILED) // 存储交易信息到上下文 .variable("transaction", transaction) // WV -> V: 验证通过 .transition() .from(DepositStatus.WAIT_VERIFY) .to(DepositStatus.VERIFIED) .on(DepositEvent.VERIFY_PASS) .guard(ctx -> { // 检查交易金额是否合法 DepositTransaction tx = ctx.getVariable("transaction", DepositTransaction.class); return tx.getAmount() != null && tx.getAmount().compareTo(BigDecimal.ZERO) > 0; }) .and() // WV -> MC: 验证失败 .transition() .from(DepositStatus.WAIT_VERIFY) .to(DepositStatus.MANUAL_CONFIRM) .on(DepositEvent.VERIFY_FAIL) .action(ctx -> { // 发送人工确认通知 notificationService.sendManualConfirmNotify(ctx.getPayload()); }) .and() // V -> P: 入金处理中 .transition() .from(DepositStatus.VERIFIED) .to(DepositStatus.PENDING) .on(DepositEvent.DEPOSIT_PENDING) .and() // V -> RP: 账户验证失败 .transition() .from(DepositStatus.VERIFIED) .to(DepositStatus.REFUND_PROCESSING) .on(DepositEvent.ACCOUNT_VALIDATE_FAIL) .action(ctx -> refundService.initiateRefund(ctx.getPayload())) .and() // P -> P: 仍在处理中（自循环） .transition() .from(DepositStatus.PENDING) .to(DepositStatus.PENDING) .on(DepositEvent.STILL_PENDING) .and() // P -> S: 入金成功 .transition() .from(DepositStatus.PENDING) .to(DepositStatus.SUCCESS) .on(DepositEvent.DEPOSIT_SUCCESS) .action(ctx -> { log.info("入金成功，更新账户余额"); // 更新账户余额等业务逻辑 }) .and() // P -> RP: 入金失败 .transition() .from(DepositStatus.PENDING) .to(DepositStatus.REFUND_PROCESSING) .on(DepositEvent.DEPOSIT_FAILED) .action(ctx -> refundService.initiateRefund(ctx.getPayload())) .and() // MC -> RP: 人工确认退款 .transition() .from(DepositStatus.MANUAL_CONFIRM) .to(DepositStatus.REFUND_PROCESSING) .on(DepositEvent.MANUAL_CONFIRM_REFUND) .action(ctx -> refundService.initiateRefund(ctx.getPayload())) .and() // MC -> RS: 人工确认退款成功 .transition() .from(DepositStatus.MANUAL_CONFIRM) .to(DepositStatus.REFUND_SUCCESS) .on(DepositEvent.MANUAL_CONFIRM_SUCCESS) .and() // MC -> MF: 人工确认失败 .transition() .from(DepositStatus.MANUAL_CONFIRM) .to(DepositStatus.MANUAL_FAILED) .on(DepositEvent.MANUAL_CONFIRM_FAILED) .and() // RP -> RS: 退款成功 .transition() .from(DepositStatus.REFUND_PROCESSING) .to(DepositStatus.REFUND_SUCCESS) .on(DepositEvent.REFUND_SUCCESS) .and() // RP -> RF: 退款失败 .transition() .from(DepositStatus.REFUND_PROCESSING) .to(DepositStatus.REFUND_FAILED) .on(DepositEvent.REFUND_FAILED) .action(ctx -> notificationService.sendManualConfirmNotify(ctx.getPayload())) .and() // RF -> RS: 人工确认退款成功 .transition() .from(DepositStatus.REFUND_FAILED) .to(DepositStatus.REFUND_SUCCESS) .on(DepositEvent.MANUAL_CONFIRM_SUCCESS) .and() // RS -> RT: 收到退票通知 .transition() .from(DepositStatus.REFUND_SUCCESS) .to(DepositStatus.RETURN_TICKET) .on(DepositEvent.RECEIVE_RETURN_TICKET) .action(ctx -> notificationService.sendManualConfirmNotify(ctx.getPayload())) .and() // RT -> RS: 人工确认退款成功 .transition() .from(DepositStatus.RETURN_TICKET) .to(DepositStatus.REFUND_SUCCESS) .on(DepositEvent.MANUAL_CONFIRM_SUCCESS) .and() // 添加状态变更监听器 .listener((from, to, event, payload) -> { log.info("状态机状态变更: {} --[{}]--> {}", from, to, event); // 可以在这里做审计日志、消息通知等 }) .build(); } } /** * 使用示例 */ @Service @Slf4j public class DepositService { private final DepositStateMachineFactory stateMachineFactory; private final DepositTransactionRepository repository; @Transactional public void processDeposit(String transactionId, DepositEvent event, Object payload) { DepositTransaction transaction = repository.findById(transactionId) .orElseThrow(() -> new IllegalArgumentException("交易不存在")); // 创建状态机 LiteStateMachine<DepositStatus, DepositEvent> stateMachine = stateMachineFactory.create(transaction); // 触发事件 boolean success = stateMachine.fire(event, payload); if (success) { // 更新交易状态 transaction.setStatus(stateMachine.getCurrentState()); repository.save(transaction); log.info("交易状态更新成功: {}", transaction); } else { log.warn("状态流转失败，交易ID: {}, 当前状态: {}, 事件: {}", transactionId, transaction.getStatus(), event); } } }点评： 自研方案可以完全按照业务需求定制，没有任何冗余功能。缺点是需要自己维护，出了 bug 只能自己扛。适合有一定技术积累、追求极致可控的团队。方案对比方案复杂度可扩展性性能学习成本适用场景Switch/If-Else⭐⭐⭐⭐⭐⭐⭐⭐状态少于5个的简单场景状态模式⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐状态行为复杂、需要独立测试状态枚举流转表⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐中小型项目，推荐首选Spring State Machine⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐Spring 生态、需要分布式支持自研轻量级框架⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐追求极致可控、有技术积累最佳实践建议状态枚举一定要有：不管用哪种方案，状态和事件都应该用枚举定义，避免魔法字符串流转规则要可视化：建议用 Mermaid 或者其他工具画出状态流转图，方便团队理解和评审做好审计日志：每次状态变更都要记录，方便排查问题考虑并发场景：多个线程同时触发同一笔交易的状态流转，需要加锁或者用乐观锁幂等性很重要：同一个事件重复触发，结果应该一致分离状态和业务逻辑：状态流转逻辑和业务处理逻辑要分开，职责清晰总结状态机看起来简单，写好却不容易。从最原始的 if-else 到 Spring State Machine，每种方案都有其适用场景。我的建议是：快速原型：用 Switch 或状态枚举表长期维护的项目：用状态枚举表或自研轻量级框架大型分布式系统：考虑 Spring State Machine 或者自研支持分布式的方案记住，没有最好的方案，只有最合适的方案。就像写代码一样，不是炫技，而是解决问题。如果你看到这里还没放弃，那恭喜你，你已经比大部分人更了解状态机了。现在，去把你项目里的 if-else 地狱重构一下吧！
前言：为什么你需要读这篇文章还记得那个深夜吗？你的系统被人用 admin/123456 登录了，然后删库跑路。老板问你："我们的安全措施呢？"你支支吾吾地说："我以为用户会自觉的..."朋友，醒醒吧。本文将带你深入了解Java世界的三大安全框架：Spring Security（官方嫡长子）、Apache Shiro（轻量级老将）、Sa-Token（国产新秀）。看完之后，你至少能在老板面前假装很懂安全。一、安全框架核心概念在开始之前，先搞清楚几个灵魂拷问：1.1 认证 vs 授权认证（Authentication）：你是谁？—— 验明正身 授权（Authorization）：你能干啥？—— 权限控制打个比方：认证：门卫检查你的工牌，确认你是公司员工授权：你虽然是员工，但你不能进财务室拿钱1.2 安全框架核心架构flowchart TB subgraph 客户端 A[用户请求] end subgraph 安全框架 B[认证过滤器链] C[身份认证模块] D[授权决策模块] E[会话管理] F[密码加密] end subgraph 业务系统 G[Controller] H[Service] I[数据库] end A --> B B --> C C -->|认证成功| D C -->|认证失败| J[返回401] D -->|授权通过| G D -->|授权失败| K[返回403] G --> H --> I C E C F1.3 主流安全框架对比特性Spring SecurityApache ShiroSa-Token学习曲线🔥🔥🔥🔥🔥 陡峭如珠峰🔥🔥🔥 相对平缓🔥🔥 新手友好功能完整度⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐Spring集成原生支持需要适配良好支持社区活跃度非常活跃一般国内活跃OAuth2支持原生支持需要扩展内置支持文档质量英文为主,较全一般中文友好适用场景企业级复杂系统中小型项目快速开发二、Spring Security：官方嫡长子2.1 简介Spring Security 是 Spring 家族的亲儿子，功能强大到令人发指，配置复杂到令人放弃。但没办法，谁让人家是正统呢？"Spring Security 的学习曲线不是曲线，是悬崖。" —— 某不愿透露姓名的秃头程序员2.2 核心架构flowchart TB subgraph FilterChain[安全过滤器链] F1[SecurityContextPersistenceFilter安全上下文持久化] F2[UsernamePasswordAuthenticationFilter用户名密码认证] F3[BasicAuthenticationFilterBasic认证] F4[ExceptionTranslationFilter异常转换] F5[FilterSecurityInterceptor授权拦截器] end subgraph Core[核心组件] A[AuthenticationManager认证管理器] P[AuthenticationProvider认证提供者] U[UserDetailsService用户详情服务] E[PasswordEncoder密码编码器] end subgraph Authorization[授权组件] AD[AccessDecisionManager访问决策管理器] V1[AffirmativeBased一票通过] V2[ConsensusBased少数服从多数] V3[UnanimousBased一票否决] end F1 --> F2 --> F3 --> F4 --> F5 F2 --> A A --> P P --> U P --> E F5 --> AD AD --> V1 AD --> V2 AD --> V32.3 快速入门2.3.1 添加依赖<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>恭喜你！加完这一行，你的所有接口都需要登录了。是不是很惊喜？😏2.3.2 基础配置（Spring Boot 3.x 风格）@Configuration @EnableWebSecurity @EnableMethodSecurity // 开启方法级别安全 public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http // 关闭CSRF（前后端分离项目通常关闭） .csrf(csrf -> csrf.disable()) // 配置请求授权规则 .authorizeHttpRequests(auth -> auth // 放行登录、注册接口 .requestMatchers("/auth/**", "/public/**").permitAll() // Swagger放行 .requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll() // 管理员接口 .requestMatchers("/admin/**").hasRole("ADMIN") // 其他请求需要认证 .anyRequest().authenticated() ) // 配置表单登录 .formLogin(form -> form .loginProcessingUrl("/auth/login") .successHandler(authenticationSuccessHandler()) .failureHandler(authenticationFailureHandler()) ) // 配置登出 .logout(logout -> logout .logoutUrl("/auth/logout") .logoutSuccessHandler(logoutSuccessHandler()) ) // 异常处理 .exceptionHandling(ex -> ex .authenticationEntryPoint(authenticationEntryPoint()) .accessDeniedHandler(accessDeniedHandler()) ) // Session配置（无状态） .sessionManagement(session -> session .sessionCreationPolicy(SessionCreationPolicy.STATELESS) ); // 添加JWT过滤器 http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); return http.build(); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public AuthenticationManager authenticationManager( AuthenticationConfiguration config) throws Exception { return config.getAuthenticationManager(); } }2.3.3 自定义 UserDetailsService@Service @RequiredArgsConstructor public class CustomUserDetailsService implements UserDetailsService { private final UserRepository userRepository; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // 从数据库查询用户 User user = userRepository.findByUsername(username) .orElseThrow(() -> new UsernameNotFoundException( "用户不存在: " + username)); // 构建Spring Security的UserDetails return org.springframework.security.core.userdetails.User .withUsername(user.getUsername()) .password(user.getPassword()) .authorities(user.getRoles().stream() .map(role -> new SimpleGrantedAuthority("ROLE_" + role.getName())) .collect(Collectors.toList())) .accountExpired(false) .accountLocked(user.isLocked()) .credentialsExpired(false) .disabled(!user.isEnabled()) .build(); } }2.3.4 JWT 工具类@Component public class JwtTokenProvider { @Value("${jwt.secret}") private String jwtSecret; @Value("${jwt.expiration}") private long jwtExpiration; /** * 生成JWT Token */ public String generateToken(Authentication authentication) { UserDetails userDetails = (UserDetails) authentication.getPrincipal(); Date now = new Date(); Date expiryDate = new Date(now.getTime() + jwtExpiration); return Jwts.builder() .setSubject(userDetails.getUsername()) .claim("roles", userDetails.getAuthorities().stream() .map(GrantedAuthority::getAuthority) .collect(Collectors.toList())) .setIssuedAt(now) .setExpiration(expiryDate) .signWith(getSigningKey(), SignatureAlgorithm.HS512) .compact(); } /** * 从Token中获取用户名 */ public String getUsernameFromToken(String token) { Claims claims = Jwts.parserBuilder() .setSigningKey(getSigningKey()) .build() .parseClaimsJws(token) .getBody(); return claims.getSubject(); } /** * 验证Token */ public boolean validateToken(String token) { try { Jwts.parserBuilder() .setSigningKey(getSigningKey()) .build() .parseClaimsJws(token); return true; } catch (JwtException | IllegalArgumentException e) { log.error("Invalid JWT token: {}", e.getMessage()); return false; } } private Key getSigningKey() { byte[] keyBytes = Decoders.BASE64.decode(jwtSecret); return Keys.hmacShaKeyFor(keyBytes); } }2.3.5 JWT 认证过滤器@Component @RequiredArgsConstructor public class JwtAuthenticationFilter extends OncePerRequestFilter { private final JwtTokenProvider tokenProvider; private final CustomUserDetailsService userDetailsService; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { try { String jwt = getJwtFromRequest(request); if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) { String username = tokenProvider.getUsernameFromToken(jwt); UserDetails userDetails = userDetailsService .loadUserByUsername(username); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); authentication.setDetails( new WebAuthenticationDetailsSource().buildDetails(request)); // 将认证信息放入Security上下文 SecurityContextHolder.getContext() .setAuthentication(authentication); } } catch (Exception e) { log.error("无法设置用户认证: {}", e.getMessage()); } filterChain.doFilter(request, response); } private String getJwtFromRequest(HttpServletRequest request) { String bearerToken = request.getHeader("Authorization"); if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { return bearerToken.substring(7); } return null; } }2.4 方法级别权限控制@RestController @RequestMapping("/api/users") @RequiredArgsConstructor public class UserController { private final UserService userService; // 需要ADMIN角色 @PreAuthorize("hasRole('ADMIN')") @GetMapping public List<UserDTO> getAllUsers() { return userService.findAll(); } // 需要USER或ADMIN角色 @PreAuthorize("hasAnyRole('USER', 'ADMIN')") @GetMapping("/{id}") public UserDTO getUser(@PathVariable Long id) { return userService.findById(id); } // 只能访问自己的信息，或者是管理员 @PreAuthorize("#id == authentication.principal.id or hasRole('ADMIN')") @PutMapping("/{id}") public UserDTO updateUser(@PathVariable Long id, @RequestBody UserDTO dto) { return userService.update(id, dto); } // 基于权限（Permission）的控制 @PreAuthorize("hasAuthority('user:delete')") @DeleteMapping("/{id}") public void deleteUser(@PathVariable Long id) { userService.delete(id); } // 方法执行后校验返回值 @PostAuthorize("returnObject.createdBy == authentication.name") @GetMapping("/my-data/{id}") public DataDTO getMyData(@PathVariable Long id) { return dataService.findById(id); } }2.5 Spring Security 认证流程详解sequenceDiagram participant C as 客户端 participant F as JwtAuthFilter participant SC as SecurityContext participant AM as AuthenticationManager participant AP as AuthenticationProvider participant UDS as UserDetailsService participant DB as 数据库 C->>F: 1. 携带Token请求 F->>F: 2. 解析Token获取username F->>UDS: 3. loadUserByUsername() UDS->>DB: 4. 查询用户 DB-->>UDS: 5. 返回用户信息 UDS-->>F: 6. 返回UserDetails F->>SC: 7. 设置Authentication SC-->>C: 8. 请求继续执行 Note over C,DB: 登录流程 C->>AM: 9. authenticate(用户名,密码) AM->>AP: 10. 委托认证 AP->>UDS: 11. 加载用户 UDS->>DB: 12. 查询 DB-->>UDS: 13. 用户数据 UDS-->>AP: 14. UserDetails AP->>AP: 15. 密码比对 AP-->>AM: 16. Authentication AM-->>C: 17. 认证成功,返回Token三、Apache Shiro：轻量级老将3.1 简介Apache Shiro 是一个老牌安全框架，比 Spring Security 更轻量，学习曲线更平缓。如果你觉得 Spring Security 太重了，Shiro 是个不错的选择。"Shiro 的 API 设计得像人话。" —— 被 Spring Security 虐过的程序员3.2 核心架构flowchart TB subgraph Subject[Subject 当前用户] S[Subject] end subgraph SecurityManager[SecurityManager 安全管理器] SM[SecurityManager] AU[Authenticator认证器] AZ[Authorizer授权器] SEM[SessionManager会话管理器] end subgraph Realms[Realm 数据源] R1[JdbcRealm] R2[LdapRealm] R3[CustomRealm] end subgraph Support[支撑组件] CC[CacheManager缓存管理] CR[Cryptography加密] end S --> SM SM --> AU SM --> AZ SM --> SEM AU --> R1 AU --> R2 AU --> R3 AZ --> R1 AZ --> R2 AZ --> R3 SM --> CC SM --> CR3.3 快速入门3.3.1 添加依赖<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring-boot-starter</artifactId> <version>1.13.0</version> </dependency>3.3.2 自定义 Realmpublic class CustomRealm extends AuthorizingRealm { @Autowired private UserService userService; /** * 授权（权限验证） */ @Override protected AuthorizationInfo doGetAuthorizationInfo( PrincipalCollection principals) { String username = (String) principals.getPrimaryPrincipal(); User user = userService.findByUsername(username); SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); // 设置角色 Set<String> roles = user.getRoles().stream() .map(Role::getName) .collect(Collectors.toSet()); info.setRoles(roles); // 设置权限 Set<String> permissions = user.getRoles().stream() .flatMap(role -> role.getPermissions().stream()) .map(Permission::getCode) .collect(Collectors.toSet()); info.setStringPermissions(permissions); return info; } /** * 认证（身份验证） */ @Override protected AuthenticationInfo doGetAuthenticationInfo( AuthenticationToken token) throws AuthenticationException { String username = (String) token.getPrincipal(); User user = userService.findByUsername(username); if (user == null) { throw new UnknownAccountException("用户不存在"); } if (user.isLocked()) { throw new LockedAccountException("账户已锁定"); } // 返回认证信息，Shiro会自动比对密码 return new SimpleAuthenticationInfo( user.getUsername(), user.getPassword(), ByteSource.Util.bytes(user.getSalt()), // 盐值 getName() ); } }3.3.3 Shiro 配置@Configuration public class ShiroConfig { @Bean public CustomRealm customRealm() { CustomRealm realm = new CustomRealm(); // 设置密码匹配器 HashedCredentialsMatcher matcher = new HashedCredentialsMatcher(); matcher.setHashAlgorithmName("SHA-256"); matcher.setHashIterations(1024); realm.setCredentialsMatcher(matcher); return realm; } @Bean public DefaultWebSecurityManager securityManager() { DefaultWebSecurityManager manager = new DefaultWebSecurityManager(); manager.setRealm(customRealm()); manager.setSessionManager(sessionManager()); manager.setCacheManager(cacheManager()); return manager; } @Bean public ShiroFilterFactoryBean shiroFilter() { ShiroFilterFactoryBean filter = new ShiroFilterFactoryBean(); filter.setSecurityManager(securityManager()); // 登录页面 filter.setLoginUrl("/login"); // 未授权页面 filter.setUnauthorizedUrl("/403"); // 过滤器链配置 Map<String, String> filterChainMap = new LinkedHashMap<>(); filterChainMap.put("/static/**", "anon"); // 静态资源放行 filterChainMap.put("/login", "anon"); // 登录放行 filterChainMap.put("/logout", "logout"); // 登出 filterChainMap.put("/admin/**", "roles[admin]"); // admin角色 filterChainMap.put("/**", "authc"); // 其他需要认证 filter.setFilterChainDefinitionMap(filterChainMap); return filter; } @Bean public SessionManager sessionManager() { DefaultWebSessionManager manager = new DefaultWebSessionManager(); manager.setSessionIdUrlRewritingEnabled(false); manager.setGlobalSessionTimeout(1800000); // 30分钟 return manager; } @Bean public CacheManager cacheManager() { return new MemoryConstrainedCacheManager(); } // 开启注解支持 @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() { AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor(); advisor.setSecurityManager(securityManager()); return advisor; } }3.3.4 使用示例@RestController @RequestMapping("/api") public class ApiController { /** * 登录 */ @PostMapping("/login") public Result login(@RequestBody LoginRequest request) { Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken( request.getUsername(), request.getPassword(), request.isRememberMe() ); try { subject.login(token); return Result.success("登录成功"); } catch (UnknownAccountException e) { return Result.fail("用户不存在"); } catch (IncorrectCredentialsException e) { return Result.fail("密码错误"); } catch (LockedAccountException e) { return Result.fail("账户已锁定"); } } /** * 需要认证 */ @RequiresAuthentication @GetMapping("/user/info") public Result getUserInfo() { Subject subject = SecurityUtils.getSubject(); String username = (String) subject.getPrincipal(); return Result.success(userService.findByUsername(username)); } /** * 需要角色 */ @RequiresRoles("admin") @GetMapping("/admin/users") public Result listUsers() { return Result.success(userService.findAll()); } /** * 需要权限 */ @RequiresPermissions("user:delete") @DeleteMapping("/user/{id}") public Result deleteUser(@PathVariable Long id) { userService.delete(id); return Result.success(); } /** * 多个角色（满足其一） */ @RequiresRoles(value = {"admin", "manager"}, logical = Logical.OR) @GetMapping("/reports") public Result getReports() { return Result.success(reportService.findAll()); } }3.4 Shiro 过滤器说明过滤器说明示例anon匿名访问，不需要认证/public/**authc需要认证/**logout登出过滤器/logoutroles需要指定角色roles[admin]perms需要指定权限perms[user:read]user已认证或RememberMe/user/**ssl需要HTTPS/secure/**四、Sa-Token：国产新秀4.1 简介Sa-Token 是一个轻量级 Java 权限认证框架，主打的就是一个简单。如果你厌倦了 Spring Security 的繁琐配置，Sa-Token 绝对能让你眼前一亮。"终于有一个看得懂的安全框架了！" —— 刚从 Spring Security 逃出来的程序员4.2 核心特性mindmap root((Sa-Token)) 登录认证 多端登录 单点登录SSO OAuth2.0 记住我 权限认证 角色认证 权限认证 会话查询 踢人下线 Session会话 Token风格 自动续签 会话治理 其他功能 微服务鉴权 临时Token 二级认证 密码加密4.3 快速入门4.3.1 添加依赖 <dependency> <groupId>cn.dev33</groupId> <artifactId>sa-token-spring-boot3-starter</artifactId> <version>1.38.0</version> </dependency> <dependency> <groupId>cn.dev33</groupId> <artifactId>sa-token-redis-jackson</artifactId> <version>1.38.0</version> </dependency>4.3.2 配置文件sa-token: # Token名称（同时也是Cookie名称） token-name: Authorization # Token有效期（秒），-1代表永不过期 timeout: 86400 # Token临时有效期（指定时间内无操作则过期） active-timeout: 1800 # 是否允许同一账号并发登录 is-concurrent: true # 同一账号最大登录数量 max-login-count: 5 # Token风格 token-style: uuid # 是否输出操作日志 is-log: true4.3.3 登录认证@RestController @RequestMapping("/auth") public class AuthController { @Autowired private UserService userService; /** * 登录 —— 就这么简单！ */ @PostMapping("/login") public SaResult login(@RequestBody LoginRequest req) { User user = userService.findByUsername(req.getUsername()); if (user == null) { return SaResult.error("用户不存在"); } if (!passwordEncoder.matches(req.getPassword(), user.getPassword())) { return SaResult.error("密码错误"); } // 登录：一行代码搞定！ StpUtil.login(user.getId()); // 返回Token return SaResult.data(StpUtil.getTokenValue()); } /** * 登出 */ @PostMapping("/logout") public SaResult logout() { StpUtil.logout(); return SaResult.ok("登出成功"); } /** * 查询登录状态 */ @GetMapping("/is-login") public SaResult isLogin() { return SaResult.ok("是否登录: " + StpUtil.isLogin()); } /** * 获取当前登录用户信息 */ @GetMapping("/info") public SaResult info() { // 获取当前登录用户ID long userId = StpUtil.getLoginIdAsLong(); User user = userService.findById(userId); return SaResult.data(user); } }4.3.4 权限认证首先，实现权限接口：@Component public class StpInterfaceImpl implements StpInterface { @Autowired private UserService userService; /** * 返回用户的权限列表 */ @Override public List<String> getPermissionList(Object loginId, String loginType) { Long userId = Long.parseLong(loginId.toString()); User user = userService.findById(userId); return user.getRoles().stream() .flatMap(role -> role.getPermissions().stream()) .map(Permission::getCode) .distinct() .collect(Collectors.toList()); } /** * 返回用户的角色列表 */ @Override public List<String> getRoleList(Object loginId, String loginType) { Long userId = Long.parseLong(loginId.toString()); User user = userService.findById(userId); return user.getRoles().stream() .map(Role::getName) .collect(Collectors.toList()); } }然后愉快地使用：@RestController @RequestMapping("/api") public class ApiController { /** * 角色校验 */ @GetMapping("/admin/data") public SaResult adminData() { // 校验当前用户是否有admin角色 StpUtil.checkRole("admin"); return SaResult.data("管理员数据"); } /** * 权限校验 */ @GetMapping("/user/delete") public SaResult deleteUser() { // 校验当前用户是否有user:delete权限 StpUtil.checkPermission("user:delete"); return SaResult.ok("删除成功"); } /** * 多角色校验（满足其一） */ @GetMapping("/reports") public SaResult reports() { StpUtil.checkRoleOr("admin", "manager", "analyst"); return SaResult.data("报表数据"); } /** * 多权限校验（全部满足） */ @GetMapping("/sensitive/operation") public SaResult sensitiveOp() { StpUtil.checkPermissionAnd("data:read", "data:write", "data:export"); return SaResult.ok("敏感操作执行成功"); } }4.3.5 注解鉴权@RestController @RequestMapping("/api") public class AnnotationController { // 登录校验 @SaCheckLogin @GetMapping("/user/info") public SaResult userInfo() { return SaResult.data("用户信息"); } // 角色校验 @SaCheckRole("admin") @GetMapping("/admin/settings") public SaResult adminSettings() { return SaResult.data("管理员设置"); } // 权限校验 @SaCheckPermission("user:add") @PostMapping("/user") public SaResult addUser() { return SaResult.ok("添加成功"); } // 多角色校验（mode = OR 表示满足其一） @SaCheckRole(value = {"admin", "manager"}, mode = SaMode.OR) @GetMapping("/dashboard") public SaResult dashboard() { return SaResult.data("仪表盘数据"); } // 二级认证（敏感操作前需要再次验证） @SaCheckSafe @PostMapping("/transfer") public SaResult transfer() { return SaResult.ok("转账成功"); } }4.4 高级功能4.4.1 多端登录// 在不同设备登录时，指定设备类型 StpUtil.login(10001, "PC"); // PC端登录 StpUtil.login(10001, "APP"); // APP端登录 StpUtil.login(10001, "WeChat"); // 微信小程序登录 // 踢掉指定设备 StpUtil.logout(10001, "PC"); // 踢掉所有设备 StpUtil.logout(10001);4.4.2 踢人下线// 踢人下线（根据用户ID） StpUtil.kickout(10001); // 踢人下线（根据Token） StpUtil.kickoutByTokenValue("xxxx-xxxx-xxxx"); // 获取某用户的所有Token List<String> tokens = StpUtil.getTokenValueListByLoginId(10001);4.4.3 同端互斥登录sa-token: # 同端互斥：同一设备类型只能登录一个 is-concurrent: false # 当登录数量超出时，是否踢出旧Token is-share: false五、OAuth 2.0 集成5.1 OAuth 2.0 简介OAuth 2.0 是一个开放授权标准，允许用户授权第三方应用访问其资源，而无需暴露密码。sequenceDiagram participant U as 用户 participant C as 客户端应用 participant A as 授权服务器 participant R as 资源服务器 U->>C: 1. 点击"使用GitHub登录" C->>A: 2. 重定向到授权页面 A->>U: 3. 显示授权页面 U->>A: 4. 用户同意授权 A->>C: 5. 返回授权码(code) C->>A: 6. 用授权码换取Token A->>C: 7. 返回Access Token C->>R: 8. 携带Token请求资源 R->>C: 9. 返回受保护资源 C->>U: 10. 显示资源5.2 Spring Security OAuth2 客户端@Configuration @EnableWebSecurity public class OAuth2ClientConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth .requestMatchers("/", "/login/**").permitAll() .anyRequest().authenticated() ) .oauth2Login(oauth2 -> oauth2 .loginPage("/login") .defaultSuccessUrl("/home") .userInfoEndpoint(userInfo -> userInfo .userService(customOAuth2UserService()) ) ); return http.build(); } @Bean public OAuth2UserService<OAuth2UserRequest, OAuth2User> customOAuth2UserService() { return new CustomOAuth2UserService(); } }配置文件：spring: security: oauth2: client: registration: github: client-id: your-client-id client-secret: your-client-secret scope: read:user,user:email google: client-id: your-google-client-id client-secret: your-google-client-secret scope: openid,profile,email5.3 Sa-Token OAuth2 服务端@Configuration public class SaOAuth2Config { @Autowired public void configOAuth2Server(SaOAuth2Config oauth2Config) { // 配置OAuth2模式 oauth2Config // 授权码模式 .setEnableAuthorizationCode(true) // 隐式模式 .setEnableImplicit(true) // 密码模式 .setEnablePassword(true) // 客户端模式 .setEnableClientCredentials(true); } } @RestController @RequestMapping("/oauth2") public class OAuth2Controller { // 授权端点 @RequestMapping("/authorize") public Object authorize() { return SaOAuth2Handle.serverRequest(); } // Token端点 @RequestMapping("/token") public Object token() { return SaOAuth2Handle.serverRequest(); } }六、安全最佳实践6.1 密码安全@Component public class PasswordHelper { // 使用BCrypt（推荐） private final BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(12); /** * 加密密码 */ public String encode(String rawPassword) { return encoder.encode(rawPassword); } /** * 验证密码 */ public boolean matches(String rawPassword, String encodedPassword) { return encoder.matches(rawPassword, encodedPassword); } /** * 密码强度检查 */ public boolean isStrongPassword(String password) { // 至少8位，包含大小写字母、数字、特殊字符 String pattern = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]{8,}$"; return password.matches(pattern); } }6.2 防止常见攻击@Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http // CSRF保护（前后端分离可以关闭，但要使用其他方式） .csrf(csrf -> csrf .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) ) // 安全响应头 .headers(headers -> headers // 防止点击劫持 .frameOptions(frame -> frame.deny()) // XSS保护 .xssProtection(xss -> xss.enable()) // 内容类型嗅探 .contentTypeOptions(content -> {}) // HSTS .httpStrictTransportSecurity(hsts -> hsts .includeSubDomains(true) .maxAgeInSeconds(31536000) ) ) // 其他配置... ; return http.build(); } }6.3 登录安全策略@Service @RequiredArgsConstructor public class LoginSecurityService { private final StringRedisTemplate redisTemplate; private static final int MAX_ATTEMPTS = 5; private static final int LOCK_MINUTES = 30; /** * 检查是否被锁定 */ public boolean isLocked(String username) { String key = "login:locked:" + username; return Boolean.TRUE.equals(redisTemplate.hasKey(key)); } /** * 记录失败次数 */ public void recordFailure(String username) { String key = "login:attempts:" + username; Long attempts = redisTemplate.opsForValue().increment(key); redisTemplate.expire(key, LOCK_MINUTES, TimeUnit.MINUTES); if (attempts != null && attempts >= MAX_ATTEMPTS) { lock(username); } } /** * 锁定账户 */ public void lock(String username) { String key = "login:locked:" + username; redisTemplate.opsForValue().set(key, "1", LOCK_MINUTES, TimeUnit.MINUTES); } /** * 清除失败记录（登录成功后调用） */ public void clearFailures(String username) { redisTemplate.delete("login:attempts:" + username); redisTemplate.delete("login:locked:" + username); } }6.4 敏感数据脱敏public class DataMasker { /** * 手机号脱敏：138****8888 */ public static String maskPhone(String phone) { if (phone == null || phone.length() != 11) { return phone; } return phone.substring(0, 3) + "****" + phone.substring(7); } /** * 身份证脱敏：110***********1234 */ public static String maskIdCard(String idCard) { if (idCard == null || idCard.length() < 8) { return idCard; } return idCard.substring(0, 3) + "***********" + idCard.substring(idCard.length() - 4); } /** * 邮箱脱敏：t***@example.com */ public static String maskEmail(String email) { if (email == null || !email.contains("@")) { return email; } int atIndex = email.indexOf("@"); if (atIndex <= 1) { return email; } return email.charAt(0) + "***" + email.substring(atIndex); } /** * 银行卡脱敏：**** **** **** 1234 */ public static String maskBankCard(String cardNo) { if (cardNo == null || cardNo.length() < 4) { return cardNo; } return "**** **** **** " + cardNo.substring(cardNo.length() - 4); } }七、框架选型建议flowchart TD A[项目类型?] --> B{企业级复杂系统?} B -->|是| C[Spring Security] B -->|否| D{需要快速开发?} D -->|是| E[Sa-Token] D -->|否| F{团队熟悉Shiro?} F -->|是| G[Apache Shiro] F -->|否| H{有Spring环境?} H -->|是| I[Spring Security或 Sa-Token] H -->|否| J[Apache Shiro] C --> K[功能最全,生态最好学习成本高] E --> L[简单易用,国内友好功能够用] G --> M[轻量灵活社区一般]选型总结场景推荐框架理由Spring Boot企业项目Spring Security官方支持，功能全面快速原型/中小项目Sa-Token上手快，中文文档友好非Spring项目Apache Shiro框架无关，轻量灵活需要OAuth2/OIDCSpring Security原生支持最完善微服务架构Sa-Token / Spring Security都有微服务方案学习入门Sa-Token门槛最低，概念清晰八、总结安全框架就像保险，平时觉得没用，出事了追悔莫及。核心要点：认证和授权是两码事：先确认你是谁，再决定你能干啥选择适合的框架：别用牛刀杀鸡，也别用水果刀砍牛安全是系统工程：框架只是一部分，还需要配合其他安全措施持续学习：安全领域日新月异，今天的最佳实践可能明天就过时了最后，无论选择哪个框架，记住这句话："最大的安全漏洞永远是人。" —— 鲁迅（可能没说过）参考资料Spring Security 官方文档Apache Shiro 官方文档Sa-Token 官方文档OAuth 2.0 规范JWT 官方网站
🛠️ "工欲善其事，必先利其器。然后你会发现，光有器不行，还得会用。"前言：为什么工具很重要还记得刚学Java的时候吗？用记事本写代码，手动 javac 编译，然后 java 运行。那种纯粹的快乐，大概持续了...三天。然后你发现：写错一个分号，编译报错一百行想重构一个方法名，要改二十个文件调试全靠 System.out.println打包发布全靠玄学于是你开始寻找工具，然后发现工具比代码还多...本文将系统性地介绍Java开发中的各类工具，帮你从"工具困难症"中解脱出来。一、工具全景图mindmap root((Java开发工具)) IDE集成开发环境 IntelliJ IDEA Eclipse VS Code 构建工具 Maven Gradle Ant 版本控制 Git SVN 代码质量 SonarQube Checkstyle SpotBugs PMD 调试诊断 Arthas JProfiler VisualVM JConsole API测试 Postman Apifox Bruno 数据库工具 DBeaver Navicat DataGrip 容器部署 Docker Kubernetes 文档工具 Swagger Javadoc Markdown 其他工具 Lombok MapStruct JRebel二、IDE：你的第二个家2.1 IntelliJ IDEA —— 宇宙第一IDE"用过IDEA的人，都不想再用别的了。" —— 真实的用户反馈核心优势特性说明智能补全不只是补全，是读心术重构能力改名、提取、移动，一键搞定调试器强大到可以回退执行Git集成图形化操作，告别命令行恐惧插件生态想要啥都有必装插件清单📦 效率提升类 ├── Lombok # 注解生成getter/setter ├── MyBatisX # MyBatis增强，XML与Mapper跳转 ├── GenerateAllSetter # 一键生成所有setter调用 ├── GsonFormatPlus # JSON转Java实体类 ├── String Manipulation # 字符串格式转换神器 └── Key Promoter X # 快捷键提示，帮你戒掉鼠标 📦 代码质量类 ├── SonarLint # 代码质量实时检查 ├── Alibaba Java Coding Guidelines # 阿里巴巴编码规范 ├── CheckStyle-IDEA # 代码风格检查 └── SpotBugs # Bug静态分析 📦 工具增强类 ├── Maven Helper # Maven依赖分析/冲突解决 ├── JRebel # 热部署神器（付费） ├── RestfulTool # RESTful接口快速测试 ├── Translation # 翻译插件，英文文档救星 └── Rainbow Brackets # 彩虹括号，嵌套不迷路 📦 美化类 ├── One Dark Theme # 暗色主题 ├── Atom Material Icons # 文件图标美化 └── Nyan Progress Bar # 彩虹猫进度条（摸鱼必备）常用快捷键速查🔥 高频快捷键（macOS / Windows） 【导航】 ⌘O / Ctrl+N → 查找类 ⌘⇧O / Ctrl+Shift+N → 查找文件 ⌘⌥O / Ctrl+Shift+Alt+N → 查找符号（方法/变量） ⌘E / Ctrl+E → 最近打开的文件 ⌘B / Ctrl+B → 跳转到定义 ⌘⌥B / Ctrl+Alt+B → 跳转到实现 ⌘U / Ctrl+U → 跳转到父类/接口 ⌘[ / Ctrl+Alt+← → 返回上一个位置 ⌘] / Ctrl+Alt+→ → 前进到下一个位置 【编辑】 ⌘D / Ctrl+D → 复制当前行 ⌘⌫ / Ctrl+Y → 删除当前行 ⌥⇧↑ / Alt+Shift+↑ → 上移当前行 ⌘/ / Ctrl+/ → 单行注释 ⌘⌥/ / Ctrl+Shift+/ → 多行注释 ⌘⌥L / Ctrl+Alt+L → 格式化代码 ⌘⌥O / Ctrl+Alt+O → 优化导入 【重构】 ⇧F6 / Shift+F6 → 重命名 ⌘⌥M / Ctrl+Alt+M → 提取方法 ⌘⌥V / Ctrl+Alt+V → 提取变量 ⌘⌥F / Ctrl+Alt+F → 提取字段 ⌘⌥C / Ctrl+Alt+C → 提取常量 【调试】 F8 → 单步跳过 F7 → 单步进入 ⇧F8 / Shift+F8 → 单步跳出 F9 → 继续执行 ⌘F8 / Ctrl+F8 → 切换断点 【生成】 ⌘N / Alt+Insert → 生成代码（构造器/getter/setter等） ⌘⌥T / Ctrl+Alt+T → 包围代码（try-catch/if等） ⌘J / Ctrl+J → 插入Live TemplateIDEA 高级技巧// 1. Postfix Completion —— 后缀补全 user.null → if (user == null) { } user.notnull → if (user != null) { } user.var → User user = user; list.for → for (Item item : list) { } list.fori → for (int i = 0; i < list.size(); i++) { } result.return → return result; exception.throw → throw exception; // 2. Live Templates —— 代码模板 sout → System.out.println(); soutv → System.out.println("变量 = " + 变量); psvm → public static void main(String[] args) { } fori → for (int i = 0; i < ; i++) { } iter → for (Object o : ) { } // 3. 自定义 Live Template // Settings → Editor → Live Templates // 示例：创建 "log" 模板 private static final Logger log = LoggerFactory.getLogger($CLASS$.class); // 设置 $CLASS$ 变量为 className()2.2 VS Code —— 轻量级选手适合场景：快速编辑配置文件前端开发轻量级Java项目Markdown写作Java开发必装扩展：Extension Pack for Java # 微软官方Java扩展包 Spring Boot Extension Pack # Spring Boot支持 Lombok Annotations Support # Lombok支持2.3 IDE对比quadrantChart title IDE选择象限图 x-axis 轻量 --> 功能丰富 y-axis 免费 --> 付费 quadrant-1 企业首选 quadrant-2 个人付费 quadrant-3 入门首选 quadrant-4 轻量免费 IntelliJ Ultimate: [0.9, 0.85] IntelliJ Community: [0.75, 0.15] Eclipse: [0.7, 0.1] VS Code: [0.3, 0.1] NetBeans: [0.5, 0.1]三、构建工具：项目的骨架3.1 Maven —— 稳如老狗"Maven虽然XML写到手软，但它就是稳。"核心概念flowchart TB subgraph 生命周期 A[clean] --> B[validate] B --> C[compile] C --> D[test] D --> E[package] E --> F[verify] F --> G[install] G --> H[deploy] end subgraph 核心要素 I[POM.xml] J[依赖管理] K[插件系统] L[仓库机制] end I --> J I --> K J --> Lpom.xml 模板<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>my-project</artifactId> <version>1.0.0-SNAPSHOT</version> <packaging>jar</packaging> <properties> <java.version>17</java.version> <maven.compiler.source>${java.version}</maven.compiler.source> <maven.compiler.target>${java.version}</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <spring-boot.version>3.2.0</spring-boot.version> <lombok.version>1.18.30</lombok.version> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>${spring-boot.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>${lombok.version}</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>${spring-boot.version}</version> </plugin> </plugins> </build> <profiles> <profile> <id>dev</id> <properties> <env>dev</env> </properties> <activation> <activeByDefault>true</activeByDefault> </activation> </profile> <profile> <id>prod</id> <properties> <env>prod</env> </properties> </profile> </profiles> </project>Maven 常用命令# 基础命令 mvn clean # 清理target目录 mvn compile # 编译源代码 mvn test # 运行测试 mvn package # 打包 mvn install # 安装到本地仓库 mvn deploy # 部署到远程仓库 # 组合命令 mvn clean package # 清理并打包 mvn clean install -DskipTests # 跳过测试安装 mvn clean package -P prod # 使用prod环境打包 # 依赖相关 mvn dependency:tree # 查看依赖树 mvn dependency:analyze # 分析依赖（找出未使用的） mvn versions:display-dependency-updates # 检查依赖更新 # 调试相关 mvn -X clean package # Debug模式，输出详细日志 mvn -e clean package # 显示错误堆栈 # 多模块项目 mvn -pl module-name clean package # 只构建指定模块 mvn -am -pl module-name clean package # 同时构建依赖模块Maven Wrapper（推荐）# 生成Maven Wrapper mvn wrapper:wrapper # 使用Wrapper（无需安装Maven） ./mvnw clean package # Linux/Mac mvnw.cmd clean package # Windows3.2 Gradle —— 新时代的选择"Gradle：用代码写构建脚本，真香！"build.gradle.kts（Kotlin DSL）import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { java id("org.springframework.boot") version "3.2.0" id("io.spring.dependency-management") version "1.1.4" kotlin("jvm") version "1.9.20" kotlin("plugin.spring") version "1.9.20" } group = "com.example" version = "1.0.0-SNAPSHOT" java { sourceCompatibility = JavaVersion.VERSION_17 } repositories { mavenCentral() // 阿里云镜像 maven { url = uri("https://maven.aliyun.com/repository/public") } } dependencies { implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.springframework.boot:spring-boot-starter-data-jpa") compileOnly("org.projectlombok:lombok") annotationProcessor("org.projectlombok:lombok") runtimeOnly("com.mysql:mysql-connector-j") testImplementation("org.springframework.boot:spring-boot-starter-test") } tasks.withType<KotlinCompile> { kotlinOptions { freeCompilerArgs += "-Xjsr305=strict" jvmTarget = "17" } } tasks.withType<Test> { useJUnitPlatform() } // 自定义任务 tasks.register("hello") { doLast { println("Hello from Gradle!") } }Gradle 常用命令# 基础命令 ./gradlew clean # 清理 ./gradlew build # 构建 ./gradlew test # 测试 ./gradlew bootRun # 运行Spring Boot应用 # 依赖相关 ./gradlew dependencies # 查看依赖树 ./gradlew dependencyInsight --dependency lombok # 查看特定依赖 # 任务相关 ./gradlew tasks # 列出所有任务 ./gradlew tasks --all # 列出所有任务（包括隐藏的） # 性能优化 ./gradlew build --parallel # 并行构建 ./gradlew build --build-cache # 使用构建缓存3.3 Maven vs Gradle特性MavenGradle配置语言XMLGroovy/Kotlin DSL学习曲线较低较高构建速度一般快（增量构建+缓存）灵活性约定优于配置高度灵活IDE支持优秀优秀社区生态非常成熟快速增长适用场景企业标准项目复杂构建/Android四、代码质量工具4.1 静态代码分析flowchart LR A[代码提交] --> B[SonarLintIDE实时检查] B --> C[Git Commit] C --> D[CI Pipeline] D --> E[SonarQube服务端分析] E --> F{质量门禁} F -->|通过| G[部署] F -->|失败| H[修复] H --> ASonarQube 集成 <plugin> <groupId>org.sonarsource.scanner.maven</groupId> <artifactId>sonar-maven-plugin</artifactId> <version>3.10.0.2594</version> </plugin># 执行分析 mvn sonar:sonar \ -Dsonar.projectKey=my-project \ -Dsonar.host.url=http://localhost:9000 \ -Dsonar.login=your-tokenCheckstyle 配置 <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-checkstyle-plugin</artifactId> <version>3.3.1</version> <configuration> <configLocation>checkstyle.xml</configLocation> <consoleOutput>true</consoleOutput> <failsOnError>true</failsOnError> </configuration> </plugin> <?xml version="1.0"?> <module name="Checker"> <module name="TreeWalker"> <module name="ConstantName"/> <module name="LocalVariableName"/> <module name="MethodName"/> <module name="TypeName"/> <module name="AvoidStarImport"/> <module name="OneStatementPerLine"/> <module name="EmptyBlock"/> <module name="CyclomaticComplexity"> <property name="max" value="10"/> </module> </module> <module name="FileLength"> <property name="max" value="500"/> </module> </module>SpotBugs（FindBugs继任者）<plugin> <groupId>com.github.spotbugs</groupId> <artifactId>spotbugs-maven-plugin</artifactId> <version>4.8.2.0</version> <configuration> <effort>Max</effort> <threshold>Low</threshold> <xmlOutput>true</xmlOutput> </configuration> </plugin>4.2 代码格式化Google Java Format<plugin> <groupId>com.spotify.fmt</groupId> <artifactId>fmt-maven-plugin</artifactId> <version>2.21.1</version> <executions> <execution> <goals> <goal>format</goal> </goals> </execution> </executions> </plugin>EditorConfig（统一编辑器配置）# .editorconfig root = true [*] charset = utf-8 end_of_line = lf indent_size = 4 indent_style = space insert_final_newline = true trim_trailing_whitespace = true [*.{yml,yaml}] indent_size = 2 [*.md] trim_trailing_whitespace = false五、调试诊断工具5.1 Arthas —— 线上诊断神器"有了Arthas，线上问题不用重启就能查。"安装与启动# 下载 curl -O https://arthas.aliyun.com/arthas-boot.jar # 启动（自动attach到Java进程） java -jar arthas-boot.jar常用命令速查# 🔍 基础命令 dashboard # 实时数据面板 thread # 查看线程状态 thread -n 3 # 查看最忙的3个线程 thread -b # 查看阻塞线程 jvm # JVM信息 sysprop # 系统属性 sysenv # 环境变量 # 🔍 类/方法相关 sc com.example.* # 搜索类 sm com.example.UserService # 搜索方法 jad com.example.UserService # 反编译类 # 🔍 方法监控 watch com.example.UserService getUser returnObj # 观察返回值 watch com.example.UserService getUser '{params,returnObj}' -x 2 # 观察入参和返回值 trace com.example.UserService getUser # 方法调用路径追踪 tt -t com.example.UserService getUser # 记录方法调用（TimeTunnel） tt -l # 查看记录 tt -p -i 1001 # 重放调用 # 🔍 性能分析 profiler start # 开始CPU分析 profiler stop # 停止并生成火焰图 profiler stop --format html # 生成HTML格式 # 🔍 热修复（慎用） redefine /tmp/UserService.class # 热更新类 # 🔍 其他 ognl '@java.lang.System@out.println("hello")' # 执行OGNL表达式 vmtool --action getInstances --className com.example.User # 获取对象实例Arthas 实战场景# 场景1：接口响应慢，定位耗时方法 trace com.example.OrderService createOrder '#cost > 100' # 场景2：查看方法入参和返回值 watch com.example.UserService login '{params,returnObj,throwExp}' -e -x 3 # 场景3：统计方法调用次数和RT monitor -c 5 com.example.UserService getUser # 场景4：查看Spring Bean vmtool --action getInstances \ --className org.springframework.context.ApplicationContext \ --express 'instances[0].getBean("userService")'5.2 JProfiler —— 专业性能分析flowchart TB subgraph JProfiler功能 A[CPU分析] B[内存分析] C[线程分析] D[数据库分析] E[HTTP分析] end A --> A1[热点方法] A --> A2[调用树] A --> A3[火焰图] B --> B1[堆内存快照] B --> B2[对象分配] B --> B3[GC分析] C --> C1[线程状态] C --> C2[锁分析] C --> C3[死锁检测]5.3 VisualVM —— 免费的可视化工具# JDK自带，直接运行 jvisualvm # 或者下载独立版本 # https://visualvm.github.io/功能：监控CPU、内存、线程堆Dump分析线程Dump分析采样器和分析器插件扩展5.4 JVM 命令行工具# jps - 列出Java进程 jps -l # 显示完整主类名 jps -v # 显示JVM参数 # jstack - 线程堆栈 jstack <pid> # 打印线程堆栈 jstack -l <pid> # 包含锁信息 # jmap - 内存映射 jmap -heap <pid> # 堆内存概况 jmap -histo <pid> # 对象直方图 jmap -dump:format=b,file=heap.hprof <pid> # 导出堆转储 # jstat - JVM统计 jstat -gc <pid> 1000 # 每秒输出GC统计 jstat -gcutil <pid> 1000 # GC百分比统计 # jinfo - JVM配置 jinfo <pid> # 查看所有配置 jinfo -flag MaxHeapSize <pid> # 查看特定参数 # jcmd - 统一诊断命令（推荐） jcmd <pid> VM.flags # JVM参数 jcmd <pid> Thread.print # 线程堆栈 jcmd <pid> GC.heap_info # 堆信息 jcmd <pid> GC.run # 触发GC六、API开发测试工具6.1 Postman —— API测试王者核心功能📦 Postman功能 ├── 请求构建 # GET/POST/PUT/DELETE等 ├── 环境变量 # dev/test/prod环境切换 ├── 集合管理 # API分组管理 ├── 预请求脚本 # 请求前执行JS ├── 测试脚本 # 响应后断言 ├── Mock Server # 模拟服务 ├── API文档 # 自动生成文档 └── 团队协作 # 共享集合脚本示例// Pre-request Script - 生成签名 const timestamp = Date.now(); const secret = pm.environment.get("secret"); const sign = CryptoJS.MD5(timestamp + secret).toString(); pm.environment.set("timestamp", timestamp); pm.environment.set("sign", sign); // Tests - 断言响应 pm.test("Status code is 200", function () { pm.response.to.have.status(200); }); pm.test("Response has userId", function () { var jsonData = pm.response.json(); pm.expect(jsonData.data).to.have.property('userId'); }); // 提取Token并保存 var jsonData = pm.response.json(); if (jsonData.data && jsonData.data.token) { pm.environment.set("token", jsonData.data.token); }6.2 Apifox —— 国产全能选手"Postman + Swagger + Mock = Apifox"优势：中文界面友好API设计 + 调试 + Mock + 测试一体化支持导入Swagger/OpenAPI团队协作功能完善免费版功能够用6.3 HTTP Client（IDEA内置）### 登录接口 POST http://localhost:8080/api/auth/login Content-Type: application/json { "username": "admin", "password": "123456" } > {% client.global.set("token", response.body.data.token); %} ### 获取用户信息（使用上一步的token） GET http://localhost:8080/api/user/info Authorization: Bearer {{token}} ### 带环境变量 GET {{host}}/api/users Accept: application/json七、数据库工具7.1 工具对比工具价格支持数据库特点DBeaver免费/付费80+开源，功能全面DataGrip付费主流全支持JetBrains出品，智能Navicat付费MySQL/PostgreSQL等老牌，稳定MySQL Workbench免费MySQL官方工具TablePlus付费主流全支持macOS原生，美观7.2 DBeaver 推荐配置🔧 常用设置 ├── SQL编辑器 │ ├── 自动补全（Ctrl+Space） │ ├── 格式化快捷键（Ctrl+Shift+F） │ └── 执行快捷键（Ctrl+Enter） ├── 连接配置 │ ├── 保存密码 │ ├── SSH隧道 │ └── 只读模式（生产环境） └── 数据导出 ├── CSV/Excel/JSON/SQL └── 批量导出表结构7.3 数据库版本管理Flyway// application.yml spring: flyway: enabled: true locations: classpath:db/migration baseline-on-migrate: true // 脚本命名规则：V1__Create_user_table.sql // V{版本号}__{描述}.sql-- V1__Create_user_table.sql CREATE TABLE user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL, password VARCHAR(100) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- V2__Add_email_column.sql ALTER TABLE user ADD COLUMN email VARCHAR(100);Liquibase <?xml version="1.0" encoding="UTF-8"?> <databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.9.xsd"> <changeSet id="1" author="joey"> <createTable tableName="user"> <column name="id" type="BIGINT" autoIncrement="true"> <constraints primaryKey="true"/> </column> <column name="username" type="VARCHAR(50)"> <constraints nullable="false"/> </column> </createTable> </changeSet> </databaseChangeLog>八、文档工具8.1 Swagger/OpenAPI@Configuration public class SwaggerConfig { @Bean public OpenAPI customOpenAPI() { return new OpenAPI() .info(new Info() .title("用户服务API") .version("1.0.0") .description("用户管理相关接口") .contact(new Contact() .name("Joey") .email("joey@example.com"))) .externalDocs(new ExternalDocumentation() .description("项目Wiki") .url("https://wiki.example.com")); } }@RestController @RequestMapping("/api/users") @Tag(name = "用户管理", description = "用户CRUD接口") public class UserController { @Operation(summary = "获取用户列表", description = "分页获取用户列表") @Parameters({ @Parameter(name = "page", description = "页码", example = "1"), @Parameter(name = "size", description = "每页数量", example = "10") }) @GetMapping public Result<Page<UserVO>> listUsers( @RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "10") int size) { return Result.success(userService.list(page, size)); } @Operation(summary = "创建用户") @ApiResponses({ @ApiResponse(responseCode = "200", description = "创建成功"), @ApiResponse(responseCode = "400", description = "参数错误") }) @PostMapping public Result<UserVO> createUser( @RequestBody @Valid UserCreateDTO dto) { return Result.success(userService.create(dto)); } }8.2 Javadoc 规范/** * 用户服务类，提供用户相关的业务操作。 * * 主要功能包括： * * 用户注册和登录 * 用户信息管理 * 权限验证 * * * 使用示例： * {@code * UserService userService = new UserService(); * User user = userService.findById(1L); * } * * @author Joey * @version 1.0.0 * @since 2024-01-01 * @see UserRepository */ public class UserService { /** * 根据ID查询用户。 * * @param id 用户ID，不能为null * @return 用户对象，如果不存在返回null * @throws IllegalArgumentException 如果id为null * @throws UserNotFoundException 如果用户不存在 */ public User findById(Long id) { // ... } /** * 创建新用户。 * * @param dto 用户创建参数 * @return 创建成功的用户对象 * @deprecated 使用 {@link #createUserV2(UserCreateDTO)} 代替 */ @Deprecated public User createUser(UserCreateDTO dto) { // ... } }九、效率神器9.1 Lombok —— 告别样板代码// 之前：几十行getter/setter/toString... public class User { private Long id; private String username; private String email; // ... getter/setter/toString/equals/hashCode } // 之后：几个注解搞定 @Data // getter/setter/toString/equals/hashCode @Builder // 建造者模式 @NoArgsConstructor // 无参构造 @AllArgsConstructor // 全参构造 @Slf4j // 日志对象 public class User { private Long id; private String username; private String email; } // 使用示例 User user = User.builder() .id(1L) .username("joey") .email("joey@example.com") .build(); log.info("Created user: {}", user);Lombok 注解速查注解作用@Getter/@Setter生成getter/setter@ToString生成toString@EqualsAndHashCode生成equals和hashCode@Data以上四个的组合@Builder建造者模式@NoArgsConstructor无参构造器@AllArgsConstructor全参构造器@RequiredArgsConstructorfinal字段构造器@Slf4j生成log对象@Value不可变类@SneakyThrows偷偷抛出受检异常@Cleanup自动关闭资源9.2 MapStruct —— 对象映射神器// 定义Mapper接口 @Mapper(componentModel = "spring") public interface UserMapper { UserMapper INSTANCE = Mappers.getMapper(UserMapper.class); // 简单映射 UserVO toVO(User user); // 批量映射 List<UserVO> toVOList(List<User> users); // 字段名不同时 @Mapping(source = "createTime", target = "createdAt") @Mapping(source = "updateTime", target = "updatedAt") UserVO toVOWithMapping(User user); // 忽略某些字段 @Mapping(target = "password", ignore = true) UserVO toVOWithoutPassword(User user); // 使用表达式 @Mapping(target = "fullName", expression = "java(user.getFirstName() + \" \" + user.getLastName())") UserVO toVOWithExpression(User user); // 更新已有对象 void updateUserFromDTO(UserUpdateDTO dto, @MappingTarget User user); } // 使用 User user = userRepository.findById(id); UserVO vo = UserMapper.INSTANCE.toVO(user);9.3 JRebel —— 热部署神器"改代码不用重启，节省的时间可以多喝两杯咖啡。"功能：修改Java代码即时生效修改配置文件即时生效修改资源文件即时生效支持大部分框架价格：付费（但真的很值）9.4 其他效率工具📦 开发效率工具 ├── DevToys # Windows开发者工具箱 ├── He3 # 开发者工具集合 ├── Regex101 # 正则表达式测试 ├── JSON Formatter # JSON格式化 ├── Cron表达式生成器 # Cron在线生成 └── PlantUML # 文本画UML图 📦 终端工具 ├── iTerm2 # macOS终端增强 ├── Oh My Zsh # Zsh配置框架 ├── Windows Terminal # Windows终端 └── Tmux # 终端复用 📦 效率工具 ├── Alfred/Raycast # macOS效率启动器 ├── Snipaste # 截图贴图 ├── Typora # Markdown编辑器 └── Draw.io # 流程图绘制十、工具链整合：从开发到部署flowchart LR subgraph 开发阶段 A[IDEA] --> B[编码] B --> C[Lombok/MapStruct] C --> D[本地测试] D --> E[Arthas调试] end subgraph 提交阶段 F[Git提交] --> G[Pre-commit Hook] G --> H[Checkstyle检查] H --> I[单元测试] end subgraph CI/CD J[Jenkins/GitLab CI] --> K[Maven/Gradle构建] K --> L[SonarQube扫描] L --> M[Docker打包] M --> N[K8s部署] end E --> F I --> J完整工具链示例# .gitlab-ci.yml 示例 stages: - build - test - quality - deploy variables: MAVEN_OPTS: "-Dmaven.repo.local=.m2/repository" build: stage: build image: maven:3.9-eclipse-temurin-17 script: - mvn clean package -DskipTests artifacts: paths: - target/*.jar test: stage: test script: - mvn test artifacts: reports: junit: target/surefire-reports/*.xml sonarqube: stage: quality script: - mvn sonar:sonar -Dsonar.host.url=$SONAR_HOST -Dsonar.login=$SONAR_TOKEN only: - main - develop deploy-prod: stage: deploy script: - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA . - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA - kubectl set image deployment/app app=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA only: - main when: manual十一、总结工具选择原则flowchart TD A[选择工具] --> B{团队是否已有标准?} B -->|是| C[遵循团队标准] B -->|否| D{项目规模?} D -->|小型| E[选择轻量级工具] D -->|大型| F[选择功能全面的工具] E --> G{是否免费?} F --> G G -->|需要免费| H[开源方案] G -->|可以付费| I[商业方案]工具推荐总结类别首选备选IDEIntelliJ IDEAVS Code构建Maven/Gradle-版本控制Git-代码质量SonarQube + SonarLintCheckstyle + SpotBugs调试ArthasJProfiler (付费)API测试ApifoxPostman数据库DBeaverDataGrip (付费)文档Swagger/OpenAPI-效率Lombok + MapStruct-最后的话"工具不在多，够用就好。关键是形成自己的工作流，让工具为你服务，而不是被工具绑架。"记住：先掌握核心工具：IDE、构建工具、Git，这三个必须精通按需引入其他工具：不要为了用工具而用工具持续优化工作流：定期review自己的开发效率，发现瓶颈分享和交流：好工具要分享给团队，提升整体效率参考资料IntelliJ IDEA 官方文档Maven 官方文档Gradle 官方文档Arthas 官方文档Lombok 官方文档MapStruct 官方文档
本文属于《从入门到放弃》系列，但这次我要劝你别放弃，因为不写单元测试的代码，就像没系安全带的汽车——平时感觉挺自由，出事了直接起飞。前言：一个没写单元测试的程序员的一天早上9点，产品经理说："这个需求很简单，就改一行代码。"你信了，改了。下午3点，线上告警疯狂轰炸你的钉钉。晚上11点，你还在公司排查为什么改了A模块，B模块炸了，C模块也顺便炸了。你开始怀疑人生："我明明只改了一行啊？！"这，就是没有单元测试的代价。如果你写了单元测试，这一行代码改完，跑一下测试，3秒钟就知道有没有搞砸。而不是等到线上用户帮你测试。用户测试的成本，那可是真金白银的赔偿啊兄弟！一、单元测试是什么？不就是写代码测代码吗？没错，单元测试就是用代码测试代码。但很多人对单元测试有误解，以为就是启动整个Spring容器，然后调一下接口看看返回值对不对。那叫集成测试，不叫单元测试。来看一张图，理解一下测试金字塔：graph TB subgraph 测试金字塔 E2E["🔺 E2E测试数量最少 | 成本最高 | 速度最慢"] INT["🔶 集成测试数量适中 | 成本中等 | 速度中等"] UNIT["🟢 单元测试数量最多 | 成本最低 | 速度最快"] end E2E --> INT INT --> UNIT style E2E fill:#ff6b6b,stroke:#333,stroke-width:2px,color:#fff style INT fill:#feca57,stroke:#333,stroke-width:2px,color:#333 style UNIT fill:#1dd1a1,stroke:#333,stroke-width:2px,color:#fff单元测试的特点：特性说明快毫秒级执行，跑几百个测试也就几秒钟独立不依赖数据库、Redis、MQ等外部服务可重复跑一万次结果都一样自动化CI/CD流水线自动跑单元测试测的是"单元"，在Java里，通常指的是一个方法，最多是一个类。二、Spring Boot测试全家桶Spring Boot贴心地给我们准备了一套测试工具，加个依赖就能用：<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency>这一个依赖，包含了以下豪华套餐：graph LR subgraph spring-boot-starter-test JUnit5["JUnit 5测试框架本体"] Mockito["MockitoMock神器"] AssertJ["AssertJ断言增强"] Hamcrest["Hamcrest匹配器"] JSONPath["JSONPathJSON断言"] Spring["Spring TestSpring测试支持"] end style JUnit5 fill:#6c5ce7,color:#fff style Mockito fill:#00b894,color:#fff style AssertJ fill:#0984e3,color:#fff style Hamcrest fill:#fdcb6e,color:#333 style JSONPath fill:#e17055,color:#fff style Spring fill:#00cec9,color:#fff2.1 JUnit 5 基础写法先来个最简单的例子热热身：import org.junit.jupiter.api.Test; import org.junit.jupiter.api.DisplayName; import static org.junit.jupiter.api.Assertions.*; class CalculatorTest { @Test @DisplayName("1 + 1 应该等于 2，如果不等于，那数学就完蛋了") void testAdd() { Calculator calculator = new Calculator(); int result = calculator.add(1, 1); assertEquals(2, result); } @Test @DisplayName("除以0应该抛异常，不然宇宙会爆炸") void testDivideByZero() { Calculator calculator = new Calculator(); assertThrows(ArithmeticException.class, () -> { calculator.divide(10, 0); }); } }JUnit 5常用注解速查表：注解作用使用场景@Test标记测试方法每个测试方法都要加@DisplayName给测试起个人话的名字让测试报告更易读@BeforeEach每个测试前执行初始化测试数据@AfterEach每个测试后执行清理资源@BeforeAll所有测试前执行一次重量级初始化@Disabled禁用测试临时跳过某个测试@ParameterizedTest参数化测试同一逻辑不同参数2.2 参数化测试：一次写完，多组数据假设你要测试一个判断成年的方法，难道要写18个测试？不用，参数化测试安排上：import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.ValueSource; class AgeValidatorTest { @ParameterizedTest @DisplayName("成年人判断") @CsvSource({ "17, false", "18, true", "19, true", "100, true", "0, false" }) void testIsAdult(int age, boolean expected) { AgeValidator validator = new AgeValidator(); assertEquals(expected, validator.isAdult(age)); } @ParameterizedTest @DisplayName("这些年龄都应该是成年人") @ValueSource(ints = {18, 25, 30, 65, 100}) void testAdultAges(int age) { AgeValidator validator = new AgeValidator(); assertTrue(validator.isAdult(age)); } }三、Mock：假装有对象在真实项目中，你的Service不可能是孤独的，它依赖Repository、依赖其他Service、依赖第三方API...但单元测试要"隔离"，怎么办？Mock！用假的对象替代真的依赖。这就像拍电影，男主角要从大楼跳下去，你不能真的让他跳吧？找个替身（Mock）啊！sequenceDiagram participant Test as 测试代码 participant Service as UserService participant Mock as Mock的Repository participant Real as 真实数据库 Note over Test,Real: 单元测试时 Test->>Service: 调用 getUserById(1) Service->>Mock: 调用 findById(1) Mock-->>Service: 返回预设的假数据 Service-->>Test: 返回结果 Note over Real: 数据库根本没启动！3.1 Mockito基础用法import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import java.util.Optional; import static org.mockito.Mockito.*; import static org.junit.jupiter.api.Assertions.*; @ExtendWith(MockitoExtension.class) // 启用Mockito class UserServiceTest { @Mock // 这是个假的！ private UserRepository userRepository; @Mock private EmailService emailService; @InjectMocks // 把上面的假货注入到这里 private UserService userService; @Test @DisplayName("根据ID查用户 - 用户存在") void testGetUserById_Found() { // Given: 准备数据，告诉Mock对象该怎么表演 User fakeUser = new User(1L, "张三", "zhangsan@test.com"); when(userRepository.findById(1L)).thenReturn(Optional.of(fakeUser)); // When: 执行被测方法 User result = userService.getUserById(1L); // Then: 验证结果 assertNotNull(result); assertEquals("张三", result.getName()); assertEquals("zhangsan@test.com", result.getEmail()); // 验证Mock对象的方法确实被调用了 verify(userRepository, times(1)).findById(1L); } @Test @DisplayName("根据ID查用户 - 用户不存在，应该抛异常") void testGetUserById_NotFound() { // Given when(userRepository.findById(999L)).thenReturn(Optional.empty()); // When & Then assertThrows(UserNotFoundException.class, () -> { userService.getUserById(999L); }); } }3.2 Mock的各种骚操作@Test void testMockitoFeatures() { // 1. 返回固定值 when(userRepository.findById(1L)).thenReturn(Optional.of(user)); // 2. 抛异常 when(userRepository.findById(-1L)).thenThrow(new RuntimeException("ID不能为负数！")); // 3. 根据参数动态返回（Answer） when(userRepository.findById(anyLong())).thenAnswer(invocation -> { Long id = invocation.getArgument(0); if (id > 0) { return Optional.of(new User(id, "用户" + id, "user" + id + "@test.com")); } return Optional.empty(); }); // 4. 连续调用返回不同值 when(userRepository.count()) .thenReturn(1L) // 第一次调用返回1 .thenReturn(2L) // 第二次调用返回2 .thenReturn(3L); // 第三次及以后返回3 // 5. void方法抛异常 doThrow(new RuntimeException("发送失败")) .when(emailService).sendEmail(anyString(), anyString()); // 6. 验证方法调用次数 verify(userRepository, times(1)).findById(1L); verify(userRepository, never()).deleteById(anyLong()); verify(emailService, atLeast(2)).sendEmail(anyString(), anyString()); // 7. 捕获参数 ArgumentCaptor<User> captor = ArgumentCaptor.forClass(User.class); verify(userRepository).save(captor.capture()); User savedUser = captor.getValue(); assertEquals("张三", savedUser.getName()); }3.3 @MockBean vs @Mock这俩长得像，但用的地方不一样：graph TB subgraph 纯单元测试 M["@Mock + @InjectMocks"] M1["不启动Spring容器"] M2["速度快，毫秒级"] M3["测试Service层逻辑"] end subgraph Spring集成测试 MB["@MockBean"] MB1["需要启动Spring容器"] MB2["速度慢，秒级"] MB3["测试Controller或需要Spring特性时"] end M --> M1 --> M2 --> M3 MB --> MB1 --> MB2 --> MB3 style M fill:#1dd1a1,color:#fff style MB fill:#feca57,color:#333四、Service层测试实战来个实际的例子，假设我们有个订单服务：// 被测试的Service @Service @RequiredArgsConstructor public class OrderService { private final OrderRepository orderRepository; private final ProductService productService; private final InventoryService inventoryService; private final PaymentService paymentService; @Transactional public Order createOrder(CreateOrderRequest request) { // 1. 检查商品是否存在 Product product = productService.getProductById(request.getProductId()); if (product == null) { throw new ProductNotFoundException("商品不存在"); } // 2. 检查库存 if (!inventoryService.checkStock(request.getProductId(), request.getQuantity())) { throw new InsufficientStockException("库存不足"); } // 3. 计算金额 BigDecimal totalAmount = product.getPrice() .multiply(BigDecimal.valueOf(request.getQuantity())); // 4. 创建订单 Order order = Order.builder() .userId(request.getUserId()) .productId(request.getProductId()) .quantity(request.getQuantity()) .totalAmount(totalAmount) .status(OrderStatus.PENDING) .createTime(LocalDateTime.now()) .build(); return orderRepository.save(order); } }测试代码：@ExtendWith(MockitoExtension.class) class OrderServiceTest { @Mock private OrderRepository orderRepository; @Mock private ProductService productService; @Mock private InventoryService inventoryService; @Mock private PaymentService paymentService; @InjectMocks private OrderService orderService; private CreateOrderRequest validRequest; private Product testProduct; @BeforeEach void setUp() { validRequest = CreateOrderRequest.builder() .userId(1L) .productId(100L) .quantity(2) .build(); testProduct = Product.builder() .id(100L) .name("机械键盘") .price(new BigDecimal("299.00")) .build(); } @Test @DisplayName("创建订单 - 正常流程") void testCreateOrder_Success() { // Given when(productService.getProductById(100L)).thenReturn(testProduct); when(inventoryService.checkStock(100L, 2)).thenReturn(true); when(orderRepository.save(any(Order.class))).thenAnswer(invocation -> { Order order = invocation.getArgument(0); order.setId(1L); // 模拟数据库生成ID return order; }); // When Order result = orderService.createOrder(validRequest); // Then assertNotNull(result); assertEquals(1L, result.getUserId()); assertEquals(100L, result.getProductId()); assertEquals(2, result.getQuantity()); assertEquals(new BigDecimal("598.00"), result.getTotalAmount()); // 299 * 2 assertEquals(OrderStatus.PENDING, result.getStatus()); // 验证调用链 verify(productService).getProductById(100L); verify(inventoryService).checkStock(100L, 2); verify(orderRepository).save(any(Order.class)); } @Test @DisplayName("创建订单 - 商品不存在") void testCreateOrder_ProductNotFound() { // Given when(productService.getProductById(100L)).thenReturn(null); // When & Then ProductNotFoundException exception = assertThrows( ProductNotFoundException.class, () -> orderService.createOrder(validRequest) ); assertEquals("商品不存在", exception.getMessage()); // 验证后续方法没有被调用 verify(inventoryService, never()).checkStock(anyLong(), anyInt()); verify(orderRepository, never()).save(any()); } @Test @DisplayName("创建订单 - 库存不足") void testCreateOrder_InsufficientStock() { // Given when(productService.getProductById(100L)).thenReturn(testProduct); when(inventoryService.checkStock(100L, 2)).thenReturn(false); // When & Then assertThrows(InsufficientStockException.class, () -> { orderService.createOrder(validRequest); }); verify(orderRepository, never()).save(any()); } @Test @DisplayName("创建订单 - 金额计算正确性（多组数据）") @ParameterizedTest @CsvSource({ "1, 299.00, 299.00", "2, 299.00, 598.00", "10, 99.99, 999.90", "100, 0.01, 1.00" }) void testCreateOrder_AmountCalculation(int quantity, String price, String expectedTotal) { // Given testProduct.setPrice(new BigDecimal(price)); validRequest.setQuantity(quantity); when(productService.getProductById(100L)).thenReturn(testProduct); when(inventoryService.checkStock(anyLong(), anyInt())).thenReturn(true); when(orderRepository.save(any(Order.class))).thenAnswer(i -> i.getArgument(0)); // When Order result = orderService.createOrder(validRequest); // Then assertEquals(new BigDecimal(expectedTotal), result.getTotalAmount()); } }五、Controller层测试Controller层测试用MockMvc，可以模拟HTTP请求，但不需要启动真正的服务器：@WebMvcTest(UserController.class) // 只加载Controller层 class UserControllerTest { @Autowired private MockMvc mockMvc; @MockBean // 注意这里用@MockBean，因为需要Spring容器 private UserService userService; @Autowired private ObjectMapper objectMapper; @Test @DisplayName("GET /users/{id} - 查询用户成功") void testGetUser_Success() throws Exception { // Given User user = new User(1L, "张三", "zhangsan@test.com"); when(userService.getUserById(1L)).thenReturn(user); // When & Then mockMvc.perform(get("/users/1") .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(jsonPath("$.id").value(1)) .andExpect(jsonPath("$.name").value("张三")) .andExpect(jsonPath("$.email").value("zhangsan@test.com")); } @Test @DisplayName("GET /users/{id} - 用户不存在返回404") void testGetUser_NotFound() throws Exception { // Given when(userService.getUserById(999L)) .thenThrow(new UserNotFoundException("用户不存在")); // When & Then mockMvc.perform(get("/users/999")) .andExpect(status().isNotFound()) .andExpect(jsonPath("$.message").value("用户不存在")); } @Test @DisplayName("POST /users - 创建用户") void testCreateUser() throws Exception { // Given CreateUserRequest request = new CreateUserRequest("李四", "lisi@test.com"); User createdUser = new User(2L, "李四", "lisi@test.com"); when(userService.createUser(any(CreateUserRequest.class))).thenReturn(createdUser); // When & Then mockMvc.perform(post("/users") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andExpect(status().isCreated()) .andExpect(jsonPath("$.id").value(2)) .andExpect(jsonPath("$.name").value("李四")); } @Test @DisplayName("POST /users - 参数校验失败") void testCreateUser_ValidationFailed() throws Exception { // Given: 空的name CreateUserRequest request = new CreateUserRequest("", "invalid-email"); // When & Then mockMvc.perform(post("/users") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andExpect(status().isBadRequest()); } }六、Repository层测试Repository层用@DataJpaTest，它会自动配置内存数据库（H2）：@DataJpaTest @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) // 如果要用真实数据库配置 class UserRepositoryTest { @Autowired private UserRepository userRepository; @Autowired private TestEntityManager entityManager; @Test @DisplayName("测试自定义查询方法") void testFindByEmail() { // Given User user = User.builder() .name("测试用户") .email("test@example.com") .build(); entityManager.persistAndFlush(user); // When Optional<User> found = userRepository.findByEmail("test@example.com"); // Then assertTrue(found.isPresent()); assertEquals("测试用户", found.get().getName()); } @Test @DisplayName("测试复杂查询") void testFindActiveUsersByNameContaining() { // Given entityManager.persist(new User("张三丰", "zsf@test.com", true)); entityManager.persist(new User("张三", "zs@test.com", true)); entityManager.persist(new User("张三疯", "zsf2@test.com", false)); // 非活跃 entityManager.persist(new User("李四", "ls@test.com", true)); entityManager.flush(); // When List<User> users = userRepository.findByNameContainingAndActiveTrue("张三"); // Then assertEquals(2, users.size()); assertTrue(users.stream().allMatch(User::isActive)); } }七、测试覆盖率：别让你的代码裸奔测试写了多少？够不够？用覆盖率说话。推荐使用JaCoCo：<plugin> <groupId>org.jacoco</groupId> <artifactId>jacoco-maven-plugin</artifactId> <version>0.8.10</version> <executions> <execution> <goals> <goal>prepare-agent</goal> </goals> </execution> <execution> <id>report</id> <phase>test</phase> <goals> <goal>report</goal> </goals> </execution> </executions> </plugin>运行mvn test后，在target/site/jacoco/index.html查看报告。pie showData title 理想的测试覆盖率分布 "行覆盖率 > 80%" : 80 "分支覆盖率 > 70%" : 70 "方法覆盖率 > 90%" : 90覆盖率的真相：80%覆盖率是个不错的目标100%覆盖率是个美丽的谎言（有些代码真的不用测）覆盖率高不代表测试质量高（你可以写一堆不带断言的测试）八、测试最佳实践8.1 命名规范// ❌ 不好的命名 @Test void test1() { } @Test void testUser() { } // ✅ 好的命名：被测方法_场景_期望结果 @Test void getUserById_WhenUserExists_ShouldReturnUser() { } @Test void createOrder_WhenStockInsufficient_ShouldThrowException() { } // ✅ 或者用中文@DisplayName @Test @DisplayName("创建订单 - 库存不足时应抛出异常") void testCreateOrderWithInsufficientStock() { }8.2 测试结构：Given-When-Then@Test void testTransferMoney() { // Given: 准备测试数据和Mock行为 Account from = new Account(1L, new BigDecimal("1000")); Account to = new Account(2L, new BigDecimal("500")); when(accountRepository.findById(1L)).thenReturn(Optional.of(from)); when(accountRepository.findById(2L)).thenReturn(Optional.of(to)); // When: 执行被测试的方法 accountService.transfer(1L, 2L, new BigDecimal("300")); // Then: 验证结果 assertEquals(new BigDecimal("700"), from.getBalance()); assertEquals(new BigDecimal("800"), to.getBalance()); verify(accountRepository, times(2)).save(any(Account.class)); }8.3 一个测试只测一件事// ❌ 测太多东西 @Test void testUserService() { // 测创建 User created = userService.create(request); assertNotNull(created); // 测查询 User found = userService.getById(created.getId()); assertEquals(created, found); // 测更新 userService.update(created.getId(), updateRequest); // ... // 测删除 userService.delete(created.getId()); // ... } // ✅ 每个测试专注一个场景 @Test void create_ValidRequest_Success() { } @Test void create_DuplicateEmail_ThrowException() { } @Test void getById_Exists_ReturnUser() { } @Test void getById_NotExists_ThrowException() { }8.4 测试数据构建器模式当测试数据复杂时，用Builder模式：public class UserTestDataBuilder { private Long id = 1L; private String name = "默认用户"; private String email = "default@test.com"; private boolean active = true; public static UserTestDataBuilder aUser() { return new UserTestDataBuilder(); } public UserTestDataBuilder withId(Long id) { this.id = id; return this; } public UserTestDataBuilder withName(String name) { this.name = name; return this; } public UserTestDataBuilder inactive() { this.active = false; return this; } public User build() { return new User(id, name, email, active); } } // 使用 User activeUser = aUser().withName("张三").build(); User inactiveUser = aUser().withName("李四").inactive().build();九、常见问题QAQ1: 测试代码也需要维护吗？是的！测试代码是一等公民。烂测试比没测试更可怕，因为它会给你虚假的安全感。Q2: 什么代码不需要测试？getter/setter（除非有特殊逻辑）简单的委托方法配置类纯粹的数据传输对象（DTO）Q3: Mock太多是不是有问题？如果一个类需要Mock 5个以上依赖，可能说明这个类职责太多了，考虑重构。Q4: 测试要不要测私有方法？不要。测试应该通过公有方法来覆盖私有方法。如果私有方法很难被覆盖，可能需要重构。十、总结graph LR A[写代码] --> B{有测试吗?} B -->|没有| C[裸奔中...] B -->|有| D[安心改代码] C --> E[改代码就心慌] D --> F[CI自动验证] E --> G[线上出Bug] F --> H[早点下班] G --> I[半夜改Bug] style C fill:#ff6b6b,color:#fff style D fill:#1dd1a1,color:#fff style G fill:#ff6b6b,color:#fff style H fill:#1dd1a1,color:#fff style I fill:#ff6b6b,color:#fff写单元测试确实会让你多花一些时间，但这是投资，不是成本。它会在未来某个深夜，当你需要重构一段祖传代码时，默默地保护你。它会在CI流水线上，帮你拦住那些"改了一行就爆炸"的提交。它会让你成为一个更好的程序员，因为可测试的代码，往往也是设计良好的代码。所以，从今天开始，让我们一起告别"能跑就行"，拥抱"敢改就改"！
前言：技术的"生死轮回""在这个行业里，唯一不变的就是变化本身。" —— 某位被迫学了三遍前端框架的后端程序员如果你是一个有着十年以上工作经验的 Java 程序员，那么恭喜你，你已经见证了无数技术的兴衰更替。那些曾经让你熬夜加班、掉光头发的技术，如今可能已经静静躺在技术博物馆的角落里，等待着后人前来"考古"。今天，让我们一起翻开历史的篇章，缅怀那些曾经辉煌一时、如今却逐渐淡出舞台的 Java 技术。别担心，这不是一篇让你伤感的文章——毕竟，送走它们之后，我们又迎来了更多让我们加班的新技术。第一章：被淘汰的主要原因在正式开始"悼念"之前，让我们先来分析一下，为什么这些技术会被淘汰：1. 技术发展的自然规律就像诺基亚被智能手机取代一样，技术的发展是不可阻挡的洪流。新技术往往能够提供更高的开发效率、更好的性能表现和更优雅的解决方案。当 Spring Boot 出现时，那些还在手动配置 XML 的程序员们，眼泪都流下来了——不是因为不舍，而是因为太香了。2. 安全性和可靠性的硬伤某些技术在设计之初就存在安全隐患，随着互联网的发展和攻击手段的升级，这些隐患变得越来越致命。当一个技术三天两头爆出高危漏洞时，再深厚的感情也扛不住运维的怒火。3. 复杂性——程序员的头号敌人"简单就是美，复杂就是加班。"有些技术设计得过于复杂，学习曲线陡峭得像珠穆朗玛峰，配置文件多得像是在写一部长篇小说。当程序员需要花费 80% 的时间在配置和调试上，只有 20% 的时间在实际业务开发上时，这项技术就离被淘汰不远了。4. 社区和生态的萎缩技术的生命力很大程度上取决于其社区的活跃度。当 Stack Overflow 上的相关问题越来越少，GitHub 上的 star 数开始停滞，那些曾经的技术明星就开始走向黄昏了。第二章：那些年，我们一起追过的"前任"1. Servlet/JSP：Web 开发的"老祖宗"生卒年代：1997年至今（名义上还活着，实际上已经退居二线）辉煌时刻：// 曾经，我们是这样写 Web 应用的 public class HelloServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html"); PrintWriter out = response.getWriter(); out.println(" "); out.println(" Hello, World! "); out.println(" "); } }淘汰原因：在 Java 代码里写 HTML？在 HTML 里嵌 Java 代码？这操作简直是代码界的"油水混合"JSP 的 <% %> 标签让代码变得像一锅乱炖没有前后端分离的概念，改个页面样式都要重启服务器接班人：Spring MVC + RESTful API + 前端框架（Vue/React）墓志铭："我不是被打败的，我只是太老了。"2. Struts：曾经的"一哥"生卒年代：2000年 - 2017年（Struts 2 在这一年爆出了致命漏洞）辉煌时刻：在 21 世纪的头十年，如果你不会 Struts，你都不好意思说自己是 Java Web 开发者。面试官问的第一个问题就是："讲一下 Struts 的工作流程"。 <struts-config> <form-beans> <form-bean name="loginForm" type="com.example.LoginForm"/> </form-beans> <action-mappings> <action path="/login" type="com.example.LoginAction" name="loginForm" scope="request" validate="true" input="/login.jsp"> <forward name="success" path="/welcome.jsp"/> <forward name="failure" path="/login.jsp"/> </action> </action-mappings> </struts-config>淘汰原因：配置文件多得能绕地球一圈Struts 2 的安全漏洞层出不穷，2017年的 S2-045 漏洞直接让无数企业的服务器"裸奔"架构设计过于复杂，ActionForm、Action、ActionForward... 概念多到让人头秃接班人：Spring MVC（以优雅的注解配置完胜）墓志铭："我曾经是最受欢迎的，直到安全漏洞让我身败名裂。"3. EJB（Enterprise JavaBeans）：企业级的"噩梦"生卒年代：1998年 - 2010年前后（EJB 3.0 虽然有所改善，但已经难以挽回颓势）辉煌时刻：在 Sun Microsystems 的愿景中，EJB 是构建企业级分布式应用的终极解决方案。PPT 上的架构图看起来无比优雅...// 这只是冰山一角 // 你还需要写一堆 XML 配置文件、Home 接口、Remote 接口... @Stateless @Remote(HelloRemote.class) @Local(HelloLocal.class) public class HelloBean implements HelloRemote, HelloLocal { public String sayHello() { return "Hello, EJB!"; } }淘汰原因：开发一个简单的 Hello World 需要写 N 个文件部署流程复杂得像发射火箭重量级容器（WebLogic、WebSphere）价格昂贵，启动慢得能泡一杯咖啡测试困难，单元测试几乎是不可能完成的任务接班人：Spring Framework（轻量级、易测试、控制反转）墓志铭："我不是不够强大，我只是太重了，重到程序员扛不动。"4. Java Applet：浏览器里的"失落世界"生卒年代：1995年 - 2017年（Oracle 正式宣布弃用）辉煌时刻：在 Flash 还没有一统天下之前，Java Applet 曾经是网页动态内容的希望之星。 <applet code="GameApplet.class" width="400" height="300"> Your browser doesn't support Java Applets. </applet>淘汰原因：安全漏洞多到让安全专家都怕了需要安装 Java 插件，用户体验极差启动速度慢，每次加载都像是在等待宇宙大爆炸HTML5 + JavaScript 的崛起彻底终结了它的命运各大浏览器纷纷移除对 NPAPI 插件的支持，直接判了死刑接班人：HTML5、JavaScript、WebAssembly墓志铭："我曾让网页动起来，可惜我自己却停下了脚步。"5. OSGi：模块化的"先驱者"生卒年代：1999年至今（还活着，但在应用开发领域已边缘化）辉煌时刻：OSGi 是 Java 模块化的先驱，Eclipse IDE 就是基于它构建的。// Bundle-SymbolicName: com.example.hello // Bundle-Version: 1.0.0 // Export-Package: com.example.hello.api // Import-Package: org.osgi.framework public class Activator implements BundleActivator { public void start(BundleContext context) { System.out.println("Hello, OSGi!"); } public void stop(BundleContext context) { System.out.println("Goodbye, OSGi!"); } }淘汰原因：类加载器地狱（ClassLoader Hell）——当你以为解决了类冲突，新的冲突又出现了配置复杂，对开发者不友好大多数应用不需要运行时动态安装/卸载模块的能力Java 9 推出了原生的 JPMS（Java Platform Module System）接班人：Java 9 Module System、Spring Boot 的 Fat JAR、容器化部署墓志铭："我太超前了，超前到世界还没准备好接受我。"6. XML Web Services（SOAP/WSDL）：臃肿的"老绅士"生卒年代：2000年 - 2010年代（仍在银行、政府系统中苟延残喘）辉煌时刻：在 RESTful API 出现之前，SOAP Web Services 是企业级系统集成的标准方案。 <?xml version="1.0"?> <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"> <soap:Header> </soap:Header> <soap:Body> <m:GetPrice xmlns:m="http://www.example.org/stock"> <m:StockName>IBM</m:StockName> </m:GetPrice> </soap:Body> </soap:Envelope>淘汰原因：报文臃肿，一个简单的调用可能 90% 都是 XML 标签WSDL 文件复杂到需要专门的工具来阅读对开发者不友好，调试困难性能开销大，解析 XML 本身就是一种浪费接班人：RESTful API + JSON墓志铭："我很严谨，但世界需要的是简单。"7. Hibernate Criteria API（旧版）：ORM 的"繁文缛节"生卒年代：早期 Hibernate 版本 - Hibernate 5.x（被 JPA Criteria API 取代）辉煌时刻：// 旧版 Criteria 查询 Criteria criteria = session.createCriteria(User.class); criteria.add(Restrictions.eq("name", "Joey")); criteria.add(Restrictions.gt("age", 18)); criteria.addOrder(Order.asc("createTime")); List<User> users = criteria.list();淘汰原因：类型不安全，字段名用字符串表示，拼写错误只能在运行时发现不符合 JPA 规范链式调用的方式不够优雅接班人：JPA Criteria API、QueryDSL、Spring Data JPA墓志铭："我尽力了，但类型安全才是王道。"8. Java Date/Calendar：时间处理的"噩梦"生卒年代：JDK 1.0 - JDK 8（虽然还能用，但已被官方"劝退"）辉煌时刻：// 曾经，我们是这样处理日期的 Date date = new Date(); Calendar calendar = Calendar.getInstance(); calendar.setTime(date); calendar.add(Calendar.DAY_OF_MONTH, 1); // 月份从 0 开始？？？ int month = calendar.get(Calendar.MONTH); // 0 = January, WTF? // 格式化还要用 SimpleDateFormat，还不是线程安全的 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); String formatted = sdf.format(date); // 多线程下可能出问题淘汰原因：Date 类设计糟糕，很多方法早已废弃Calendar 月份从 0 开始，这是什么反人类设计？SimpleDateFormat 线程不安全，生产事故的常客时区处理复杂且容易出错可变对象，容易被意外修改接班人：Java 8 的 java.time 包（LocalDate、LocalDateTime、ZonedDateTime）墓志铭："月份从 0 开始这件事，我知道错了，但我改不了。"9. Ant：构建工具的"原始人"生卒年代：2000年 - 2010年代初（仍有遗留系统在使用）辉煌时刻： <project name="HelloWorld" default="compile"> <property name="src.dir" value="src"/> <property name="build.dir" value="build"/> <property name="lib.dir" value="lib"/> <path id="classpath"> <fileset dir="${lib.dir}" includes="*.jar"/> </path> <target name="clean"> <delete dir="${build.dir}"/> </target> <target name="compile" depends="clean"> <mkdir dir="${build.dir}"/> <javac srcdir="${src.dir}" destdir="${build.dir}"> <classpath refid="classpath"/> </javac> </target> </project>淘汰原因：没有内置依赖管理，JAR 包要手动下载和管理XML 配置繁琐，写构建脚本像在写小说没有约定优于配置的理念，每个项目的构建脚本都可能不一样对于复杂项目，构建脚本可能比业务代码还多接班人：Maven → Gradle墓志铭："在没有包管理器的年代，我们手动管理 JAR 包，那是一段艰苦但难忘的岁月。"10. Log4j 1.x：日志界的"老前辈"生卒年代：2001年 - 2015年（官方宣布停止维护）辉煌时刻：# log4j.properties，多少人的青春回忆 log4j.rootLogger=INFO, console, file log4j.appender.console=org.apache.log4j.ConsoleAppender log4j.appender.console.layout=org.apache.log4j.PatternLayout log4j.appender.console.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n淘汰原因：架构老旧，不支持异步日志配置文件格式不够灵活性能不如后来者不再维护，安全漏洞无人修复接班人：Logback、Log4j 2.x（然后 Log4j 2 在 2021 年爆出了核弹级漏洞 Log4Shell，安全团队又哭了）墓志铭："我是日志框架的开创者，我的继任者也没让我省心。"11. Swing/AWT：桌面应用的"遗老遗少"生卒年代：1995年（AWT）/ 1998年（Swing）- 至今（名存实亡）辉煌时刻：// 曾经的桌面应用开发 public class HelloSwing extends JFrame { public HelloSwing() { setTitle("Hello Swing"); setSize(400, 300); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); JButton button = new JButton("Click Me!"); button.addActionListener(e -> JOptionPane.showMessageDialog(this, "Hello, World!")); add(button); } public static void main(String[] args) { SwingUtilities.invokeLater(() -> new HelloSwing().setVisible(true)); } }淘汰原因：界面样式老旧，像是上世纪的产物（因为它确实是）跨平台意味着在所有平台上都一样丑Web 应用和移动应用的崛起，桌面应用市场萎缩JavaFX 也没能拯救 Java 桌面开发，最终也被 Oracle 放弃接班人：Electron、Web 应用、移动应用墓志铭："Write Once, Look Ugly Everywhere."第三章：正在走向衰落的技术有些技术虽然还没有完全被淘汰，但已经显露出衰落的迹象：1. JSF（JavaServer Faces）虽然还在更新，但使用率持续下降。组件化的思想没有错，但 JSF 的实现方式太过笨重。2. XML 配置从 Spring Boot 的成功可以看出，开发者更喜欢注解和约定优于配置。XML 配置文件正在逐渐消失。3. WAR 部署传统的 WAR 包部署方式正在被 Spring Boot 的可执行 JAR 和容器化部署所取代。4. 传统 MVC 服务端渲染前后端分离已成主流，传统的服务端渲染（Thymeleaf、Freemarker）使用场景越来越少。第四章：从历史中学习技术选型的智慧不要追新过度：新技术需要时间来验证，1.0 版本的框架慎用。关注社区活跃度：没有活跃社区的技术，就像没有售后服务的产品。学习思想而非工具：Spring 可能会被淘汰，但依赖注入的思想不会。保持学习能力：今天的最佳实践，可能是明天的反面教材。程序员的生存法则public class Programmer { private Stack<String> skills = new Stack<>(); public void survive() { while (true) { String oldSkill = learnNewTechnology(); if (skills.size() > 10) { String forgotten = skills.remove(0); log.info("忘掉了: {}, 反正也用不上了", forgotten); } skills.push(oldSkill); } } private String learnNewTechnology() { // TODO: 又要学新东西了... return "某个三个月后就会被淘汰的框架"; } }结语：技术的轮回"太阳底下无新事。"技术在变，但解决问题的本质需求不变。从 Servlet 到 Spring Boot，从 SOAP 到 REST，从单体到微服务——每一次变革都是对"如何更高效地解决问题"这个命题的新回答。那些被淘汰的技术，不是失败者。它们是铺路石，是垫脚石，是站在它们肩膀上才有了今天这些"更好"的技术。作为程序员，我们要做的不是为旧技术的消亡而惋惜，而是要：理解技术演进的规律——知道为什么会变，才能预测未来会怎么变掌握不变的核心原则——设计模式、架构思想、编程范式，这些比具体框架更有价值保持开放的学习心态——今天学的技术，可能五年后就会出现在"被淘汰的技术"列表里最后，让我们向那些曾经陪伴我们的技术致敬。感谢 Struts，让我们知道了配置地狱的可怕；感谢 EJB，让我们学会了珍惜轻量级框架；感谢 Java Date，让我们明白了好的 API 设计有多重要。没有你们的"牺牲"，就没有今天这些"更好"的技术。当然，今天的"更好"，也可能是明天的"被淘汰"。这就是技术的轮回，这就是程序员的宿命。共勉。本文作者正在学习第 N 个新框架，预计该框架的生命周期为 3-5 年。
java相比c++最大的进步就是在于增加了gc不用开发者手动分配和回收内存，在虚拟机自动内存管理机制的帮助下，不再需要为每一个new操作去写配对的delete/free代码，不容易出现内存泄漏和内存溢出问题，在这个过程中控制权转移给了虚拟机VM自动完成，但是这仍然无法避免内存泄露和溢出的问题，而出了OOM如何排查解决这类问题呢？本文带您一起来探寻jvm如何自动分配回收内存、如何避开内存泄露问题和解决OOM的思路。java虚拟机jvm内存模型垃圾收集器PGC、 C4收集器gc有哪些类型一个对象在何时会被回收jvm的相关工具什么时候会出现OOM出现OOM如何排查问题参考资料《深入理解java虚拟机(第三版)》
不查不知道，一查吓一跳，jdk最新的版本已经来到了jdk17，而在2017年9月，jdk发布的版本是java9，这几年oracle玩命的每年3月、9月一年发布2个版本，导致很多开发者和公司都压力很大，今年9月份又要发布jdk18了，现在用java8的公司还比比皆是。JDK 9发布后，Oracle随即宣布Java将会以持续交付的形式和更加敏捷的研发节奏向前推进，以后JDK将会在每年的3月和9月各发布一个大版本[11]，目的就是为避免众多功能特性被集中捆绑到一个 JDK版本上而引发交付风险。这次改革确实从根源上解决了跳票问题，但也为Java的用户和发行商带 来了颇大的压力，不仅程序员感慨“Java新版本还没开始用就已经过时了”，Oracle自己对着一堆JDK版 本分支也在挠头，不知道该如何维护更新，该如何提供技术支持。Oracle的解决方案是顺理成章地终 结掉“每个JDK版本最少维护三年”的优良传统，从此以后，每六个JDK大版本中才会被划出一个长期 支持(Long Term Support，LTS)版，只有LTS版的JDK能够获得为期三年的支持和更新，普通版的 JDK就只有短短六个月的生命周期。JDK 8和JDK 11会是LTS版，再下一个就到2021年发布的JDK 17 了。现有的LTS版本和支持的维护时间版本 LTS时间jdk8 2030年12月JDK11 2026年9月JDK17 2024年9月
前言：为什么需要Apache Commons？如果你写Java时经常遇到这些场景：判断字符串是否为空，写了一堆 str != null && !str.isEmpty()复制对象属性，写了几十行getter/setter读取文件内容，try-catch-finally嵌套三层集合操作，各种for循环写到手软那么恭喜你，Apache Commons就是来拯救你的！一、Apache Commons 全家福Apache Commons是Apache软件基金会的一个项目，提供了大量可复用的Java组件。它就像一个工具箱，里面装满了各种实用工具。flowchart TB subgraph ApacheCommons[Apache Commons 全家福] subgraph 核心工具[核心工具类] Lang[Commons Lang字符串/对象/反射] Collections[Commons Collections集合增强] IO[Commons IOIO操作] end subgraph 数据处理[数据处理] BeanUtils[Commons BeanUtilsBean操作] Codec[Commons Codec编解码] CSV[Commons CSVCSV处理] Compress[Commons Compress压缩解压] end subgraph 网络相关[网络相关] HttpClient[HttpClientHTTP客户端] Net[Commons Net网络协议] Email[Commons Email邮件发送] end subgraph 其他工具[其他工具] Pool[Commons Pool对象池] DBCP[Commons DBCP数据库连接池] CLI[Commons CLI命令行解析] Config[Commons Configuration配置管理] end end1.1 版本选择指南组件最新稳定版JDK要求Maven坐标commons-lang33.14.0JDK 8+org.apache.commons:commons-lang3commons-collections44.4JDK 8+org.apache.commons:commons-collections4commons-io2.15.1JDK 8+commons-io:commons-iocommons-codec1.16.0JDK 8+commons-codec:commons-codeccommons-beanutils1.9.4JDK 8+commons-beanutils:commons-beanutilshttpclient55.3JDK 8+org.apache.httpcomponents.client5:httpclient5注意：commons-lang 和 commons-lang3 是两个不同的包！commons-lang3 是新版本，包名以 org.apache.commons.lang3 开头。二、Commons Lang3：字符串和对象操作神器这是使用频率最高的Commons组件，没有之一。2.1 Maven依赖<dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.14.0</version> </dependency>2.2 StringUtils：告别空指针flowchart LR subgraph StringUtils常用方法 A[判空系列] --> A1[isEmpty] A --> A2[isBlank] A --> A3[isNotEmpty] A --> A4[isNotBlank] B[处理系列] --> B1[trim] B --> B2[strip] B --> B3[truncate] B --> B4[abbreviate] C[判断系列] --> C1[equals] C --> C2[contains] C --> C3[startsWith] C --> C4[endsWith] D[转换系列] --> D1[join] D --> D2[split] D --> D3[replace] D --> D4[reverse] end判空方法对比（划重点！）import org.apache.commons.lang3.StringUtils; // isEmpty vs isBlank 的区别 StringUtils.isEmpty(null); // true StringUtils.isEmpty(""); // true StringUtils.isEmpty(" "); // false ← 注意！空格不算空 StringUtils.isEmpty("abc"); // false StringUtils.isBlank(null); // true StringUtils.isBlank(""); // true StringUtils.isBlank(" "); // true ← 空格也算空 StringUtils.isBlank("abc"); // false // 实际开发中，90%的情况应该用 isBlank if (StringUtils.isBlank(username)) { throw new IllegalArgumentException("用户名不能为空"); }常用字符串操作// 1. 安全的equals（再也不用担心NPE了） StringUtils.equals(null, null); // true StringUtils.equals(null, "abc"); // false StringUtils.equals("abc", "abc"); // true // 忽略大小写比较 StringUtils.equalsIgnoreCase("ABC", "abc"); // true // 2. 字符串截取（安全，不会越界） StringUtils.substring("abcdef", 2); // "cdef" StringUtils.substring("abcdef", 2, 4); // "cd" StringUtils.substring("abcdef", -2); // "ef" 负数从后往前 StringUtils.substring(null, 2); // null 不会NPE // 3. 左右填充（格式化输出神器） StringUtils.leftPad("123", 5, '0'); // "00123" StringUtils.rightPad("abc", 5, '*'); // "abc**" StringUtils.center("abc", 7, '*'); // "**abc**" // 4. 移除/替换 StringUtils.remove("abcabc", "a"); // "bcbc" StringUtils.removeStart("www.baidu.com", "www."); // "baidu.com" StringUtils.removeEnd("hello.txt", ".txt"); // "hello" // 5. 分割与连接 String[] arr = StringUtils.split("a,b,c", ","); // ["a", "b", "c"] StringUtils.join(arr, "-"); // "a-b-c" // 处理null元素 String[] arr2 = {"a", null, "c"}; StringUtils.join(arr2, ","); // "a,,c" // 6. 省略过长字符串 StringUtils.abbreviate("这是一段很长很长的文字", 10); // "这是一段很长很..." StringUtils.abbreviateMiddle("abcdefghij", "...", 6); // "ab...j"2.3 ObjectUtils：对象操作import org.apache.commons.lang3.ObjectUtils; // 1. 空值处理 ObjectUtils.defaultIfNull(null, "默认值"); // "默认值" ObjectUtils.defaultIfNull("有值", "默认值"); // "有值" // 2. 第一个非空值（类似SQL的COALESCE） ObjectUtils.firstNonNull(null, null, "第三个", "第四个"); // "第三个" // 3. 比较（null安全） ObjectUtils.compare(1, 2); // -1 ObjectUtils.compare(null, 2); // -1 (null视为最小) ObjectUtils.compare(null, null); // 0 // 4. 判空 ObjectUtils.isEmpty(null); // true ObjectUtils.isEmpty(""); // true ObjectUtils.isEmpty(new int[0]); // true ObjectUtils.isEmpty(Collections.emptyList()); // true2.4 NumberUtils：数字处理import org.apache.commons.lang3.math.NumberUtils; // 1. 安全的字符串转数字（不会抛异常） NumberUtils.toInt("123"); // 123 NumberUtils.toInt("abc"); // 0 (转换失败返回0) NumberUtils.toInt("abc", -1); // -1 (转换失败返回默认值) NumberUtils.toLong("9999999999"); // 9999999999L NumberUtils.toDouble("3.14"); // 3.14 // 2. 判断字符串是否为数字 NumberUtils.isDigits("123"); // true (纯数字) NumberUtils.isDigits("12.3"); // false NumberUtils.isParsable("12.3"); // true (可解析为数字) NumberUtils.isParsable("12.3.4"); // false NumberUtils.isCreatable("0x10"); // true (支持十六进制) // 3. 取最大/最小值 NumberUtils.max(1, 5, 3, 2, 4); // 5 NumberUtils.min(1, 5, 3, 2, 4); // 12.5 ArrayUtils：数组操作import org.apache.commons.lang3.ArrayUtils; // 1. 数组判空 ArrayUtils.isEmpty(null); // true ArrayUtils.isEmpty(new int[0]); // true ArrayUtils.isEmpty(new int[]{1}); // false // 2. 数组添加元素 int[] arr = {1, 2, 3}; int[] newArr = ArrayUtils.add(arr, 4); // [1, 2, 3, 4] int[] newArr2 = ArrayUtils.insert(1, arr, 9); // [1, 9, 2, 3] // 3. 数组删除元素 ArrayUtils.remove(arr, 1); // [1, 3] 删除索引1的元素 ArrayUtils.removeElement(arr, 2); // [1, 3] 删除值为2的元素 // 4. 查找 ArrayUtils.contains(arr, 2); // true ArrayUtils.indexOf(arr, 2); // 1 // 5. 反转和打乱 ArrayUtils.reverse(arr); // [3, 2, 1] ArrayUtils.shuffle(arr); // 随机打乱 // 6. 基本类型与包装类型转换 int[] primitiveArr = {1, 2, 3}; Integer[] boxedArr = ArrayUtils.toObject(primitiveArr); int[] backToPrimitive = ArrayUtils.toPrimitive(boxedArr); // 7. 数组转字符串 ArrayUtils.toString(new int[]{1, 2, 3}); // "{1,2,3}"2.6 DateUtils（已过时，推荐用Java 8 Time API）import org.apache.commons.lang3.time.DateUtils; import org.apache.commons.lang3.time.DateFormatUtils; // 1. 日期解析（支持多种格式） Date date = DateUtils.parseDate("2024-01-15", "yyyy-MM-dd", "yyyy/MM/dd", "yyyyMMdd"); // 2. 日期计算 Date tomorrow = DateUtils.addDays(new Date(), 1); Date nextMonth = DateUtils.addMonths(new Date(), 1); Date nextYear = DateUtils.addYears(new Date(), 1); // 3. 日期截断 Date truncated = DateUtils.truncate(new Date(), Calendar.DAY_OF_MONTH); // 4. 日期格式化 String formatted = DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss");2.7 RandomStringUtils：随机字符串import org.apache.commons.lang3.RandomStringUtils; // 1. 随机字母 RandomStringUtils.randomAlphabetic(10); // "KjHgFdSaQw" // 2. 随机数字 RandomStringUtils.randomNumeric(6); // "482951" // 3. 随机字母+数字 RandomStringUtils.randomAlphanumeric(8); // "Kj8Hg2Fd" // 4. 从指定字符中随机 RandomStringUtils.random(4, "ABCD1234"); // "B3A1" // 5. 随机ASCII RandomStringUtils.randomAscii(10); // 包含特殊字符三、Commons Collections4：集合增强3.1 Maven依赖<dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-collections4</artifactId> <version>4.4</version> </dependency>3.2 CollectionUtils：集合操作flowchart TB subgraph CollectionUtils核心功能 A[判空操作] --> A1[isEmpty] A --> A2[isNotEmpty] B[集合运算] --> B1[union 并集] B --> B2[intersection 交集] B --> B3[subtract 差集] B --> B4[disjunction 对称差集] C[查找过滤] --> C1[find] C --> C2[filter] C --> C3[select] C --> C4[exists] D[转换操作] --> D1[transform] D --> D2[collect] endimport org.apache.commons.collections4.CollectionUtils; List<Integer> list1 = Arrays.asList(1, 2, 3, 4); List<Integer> list2 = Arrays.asList(3, 4, 5, 6); // 1. 判空 CollectionUtils.isEmpty(null); // true CollectionUtils.isEmpty(new ArrayList<>()); // true CollectionUtils.isNotEmpty(list1); // true // 2. 集合运算 Collection<Integer> union = CollectionUtils.union(list1, list2); // [1, 2, 3, 4, 5, 6] 并集 Collection<Integer> intersection = CollectionUtils.intersection(list1, list2); // [3, 4] 交集 Collection<Integer> subtract = CollectionUtils.subtract(list1, list2); // [1, 2] 差集(list1有而list2没有的) Collection<Integer> disjunction = CollectionUtils.disjunction(list1, list2); // [1, 2, 5, 6] 对称差集(两边独有的) // 3. 安全添加 CollectionUtils.addIgnoreNull(list1, null); // 忽略null元素 // 4. 判断是否相等（忽略顺序） CollectionUtils.isEqualCollection( Arrays.asList(1, 2, 3), Arrays.asList(3, 2, 1) ); // trueflowchart LR subgraph 集合运算图解 subgraph List1[List1: 1,2,3,4] L1_1[1] L1_2[2] L1_3[3] L1_4[4] end subgraph List2[List2: 3,4,5,6] L2_3[3] L2_4[4] L2_5[5] L2_6[6] end subgraph Results[运算结果] R1[并集: 1,2,3,4,5,6] R2[交集: 3,4] R3[差集: 1,2] R4[对称差: 1,2,5,6] end end3.3 MapUtils：Map操作import org.apache.commons.collections4.MapUtils; Map<String, Object> map = new HashMap<>(); map.put("name", "Joey"); map.put("age", "25"); map.put("score", "98.5"); map.put("active", "true"); // 1. 安全取值（带类型转换） MapUtils.getString(map, "name"); // "Joey" MapUtils.getString(map, "notExist", "默认"); // "默认" MapUtils.getInteger(map, "age"); // 25 (自动转换) MapUtils.getIntValue(map, "age"); // 25 (返回基本类型) MapUtils.getIntValue(map, "notExist", 0); // 0 MapUtils.getDouble(map, "score"); // 98.5 MapUtils.getBoolean(map, "active"); // true // 2. 判空 MapUtils.isEmpty(null); // true MapUtils.isEmpty(new HashMap<>()); // true MapUtils.isNotEmpty(map); // true // 3. 反转Map（key-value互换） Map<String, String> original = new HashMap<>(); original.put("a", "1"); original.put("b", "2"); Map<String, String> inverted = MapUtils.invertMap(original); // {"1": "a", "2": "b"} // 4. 打印Map（调试用） MapUtils.debugPrint(System.out, "myMap", map);3.4 特殊集合类型import org.apache.commons.collections4.bag.HashBag; import org.apache.commons.collections4.bidimap.TreeBidiMap; import org.apache.commons.collections4.multimap.ArrayListValuedHashMap; // 1. Bag - 可统计元素出现次数的集合 Bag<String> bag = new HashBag<>(); bag.add("apple", 3); // 添加3个apple bag.add("banana"); bag.add("apple"); // 再添加1个apple bag.getCount("apple"); // 4 // 2. BidiMap - 双向Map（可以通过value找key） BidiMap<String, Integer> bidiMap = new TreeBidiMap<>(); bidiMap.put("one", 1); bidiMap.put("two", 2); bidiMap.get("one"); // 1 bidiMap.getKey(1); // "one" bidiMap.inverseBidiMap(); // 获取反转的Map // 3. MultiValuedMap - 一个key对应多个value MultiValuedMap<String, String> multiMap = new ArrayListValuedHashMap<>(); multiMap.put("color", "red"); multiMap.put("color", "blue"); multiMap.put("color", "green"); multiMap.get("color"); // [red, blue, green]四、Commons IO：文件操作神器4.1 Maven依赖<dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.15.1</version> </dependency>4.2 FileUtils：文件操作flowchart TB subgraph FileUtils功能 A[读写操作] --> A1[readFileToString] A --> A2[writeStringToFile] A --> A3[readLines] A --> A4[writeLines] B[复制操作] --> B1[copyFile] B --> B2[copyDirectory] B --> B3[copyURLToFile] C[删除操作] --> C1[deleteQuietly] C --> C2[deleteDirectory] C --> C3[cleanDirectory] D[其他操作] --> D1[listFiles] D --> D2[sizeOf] D --> D3[moveFile] D --> D4[touch] endimport org.apache.commons.io.FileUtils; import java.io.File; import java.nio.charset.StandardCharsets; // 1. 读取文件 // 一行代码读取文件内容，告别繁琐的流操作 String content = FileUtils.readFileToString( new File("test.txt"), StandardCharsets.UTF_8 ); // 按行读取 List<String> lines = FileUtils.readLines( new File("test.txt"), StandardCharsets.UTF_8 ); // 2. 写入文件 FileUtils.writeStringToFile( new File("output.txt"), "Hello World", StandardCharsets.UTF_8 ); // 追加写入 FileUtils.writeStringToFile( new File("output.txt"), "\nNew Line", StandardCharsets.UTF_8, true // append ); // 写入多行 FileUtils.writeLines( new File("output.txt"), Arrays.asList("line1", "line2", "line3") ); // 3. 复制文件/目录 FileUtils.copyFile( new File("source.txt"), new File("dest.txt") ); FileUtils.copyDirectory( new File("sourceDir"), new File("destDir") ); // 从URL下载文件 FileUtils.copyURLToFile( new URL("https://example.com/file.zip"), new File("downloaded.zip"), 5000, // 连接超时 10000 // 读取超时 ); // 4. 删除文件/目录 FileUtils.deleteQuietly(new File("test.txt")); // 静默删除，不抛异常 FileUtils.deleteDirectory(new File("testDir")); // 删除目录及其内容 FileUtils.cleanDirectory(new File("testDir")); // 清空目录但保留目录本身 // 5. 文件/目录大小 long fileSize = FileUtils.sizeOf(new File("large.zip")); long dirSize = FileUtils.sizeOfDirectory(new File("myDir")); String readable = FileUtils.byteCountToDisplaySize(fileSize); // "1.5 GB" // 6. 列出文件 Collection<File> files = FileUtils.listFiles( new File("myDir"), new String[]{"java", "xml"}, // 扩展名过滤 true // 递归子目录 ); // 7. 比较文件 boolean same = FileUtils.contentEquals( new File("file1.txt"), new File("file2.txt") ); // 8. 移动文件 FileUtils.moveFile( new File("source.txt"), new File("newDir/dest.txt") ); // 9. 创建文件/更新时间戳 FileUtils.touch(new File("newFile.txt"));4.3 IOUtils：流操作import org.apache.commons.io.IOUtils; // 1. 流转字符串 try (InputStream is = new FileInputStream("test.txt")) { String content = IOUtils.toString(is, StandardCharsets.UTF_8); } // 2. 流转字节数组 try (InputStream is = new FileInputStream("image.png")) { byte[] bytes = IOUtils.toByteArray(is); } // 3. 流复制 try (InputStream is = new FileInputStream("source.txt"); OutputStream os = new FileOutputStream("dest.txt")) { IOUtils.copy(is, os); } // 4. 逐行读取 try (InputStream is = new FileInputStream("test.txt")) { List<String> lines = IOUtils.readLines(is, StandardCharsets.UTF_8); } // 5. 写入流 try (OutputStream os = new FileOutputStream("test.txt")) { IOUtils.write("Hello World", os, StandardCharsets.UTF_8); } // 6. 安全关闭流（不推荐，用try-with-resources更好） IOUtils.closeQuietly(inputStream);4.4 FilenameUtils：文件名处理import org.apache.commons.io.FilenameUtils; String path = "/home/user/documents/report.pdf"; // 1. 获取文件名 FilenameUtils.getName(path); // "report.pdf" FilenameUtils.getBaseName(path); // "report" FilenameUtils.getExtension(path); // "pdf" // 2. 获取路径 FilenameUtils.getFullPath(path); // "/home/user/documents/" FilenameUtils.getPath(path); // "home/user/documents/" // 3. 路径拼接（自动处理分隔符） FilenameUtils.concat("/home/user", "file.txt"); // "/home/user/file.txt" // 4. 标准化路径（处理..和.） FilenameUtils.normalize("/home/user/../admin/./file.txt"); // "/home/admin/file.txt" // 5. 检查扩展名 FilenameUtils.isExtension("test.txt", "txt"); // true FilenameUtils.isExtension("test.txt", new String[]{"txt", "doc"}); // true // 6. 通配符匹配 FilenameUtils.wildcardMatch("test.txt", "*.txt"); // true FilenameUtils.wildcardMatch("test.txt", "test.*"); // true FilenameUtils.wildcardMatch("test.txt", "t?st.txt"); // true五、Commons BeanUtils：Bean操作5.1 Maven依赖<dependency> <groupId>commons-beanutils</groupId> <artifactId>commons-beanutils</artifactId> <version>1.9.4</version> </dependency>5.2 属性复制sequenceDiagram participant Source as 源对象 participant BeanUtils as BeanUtils participant Target as 目标对象 Source->>BeanUtils: copyProperties(dest, src) BeanUtils->>BeanUtils: 反射获取源对象属性 BeanUtils->>BeanUtils: 类型转换 BeanUtils->>Target: 设置属性值 Note over Target: 属性复制完成import org.apache.commons.beanutils.BeanUtils; import org.apache.commons.beanutils.PropertyUtils; // 假设有两个类 public class UserDTO { private String name; private String age; // 注意是String // getters and setters } public class UserEntity { private String name; private Integer age; // 注意是Integer // getters and setters } // 1. 属性复制（会进行类型转换） UserDTO dto = new UserDTO(); dto.setName("Joey"); dto.setAge("25"); UserEntity entity = new UserEntity(); BeanUtils.copyProperties(entity, dto); // 注意：目标在前，源在后！ // entity.name = "Joey", entity.age = 25 // 2. 获取/设置属性 BeanUtils.getProperty(dto, "name"); // "Joey" BeanUtils.setProperty(dto, "name", "Tom"); // 3. 嵌套属性 public class Order { private User user; } BeanUtils.getProperty(order, "user.name"); // 获取嵌套属性 // 4. PropertyUtils（不做类型转换） PropertyUtils.copyProperties(entity, dto); // 类型不匹配会报错 PropertyUtils.getProperty(dto, "name"); // 5. Map与Bean互转 Map<String, String> map = BeanUtils.describe(dto); // Bean转Map BeanUtils.populate(dto, map); // Map转Bean⚠️ 性能警告：BeanUtils基于反射，性能较差。高性能场景推荐使用：MapStruct（编译期生成代码）Spring BeanUtils（相对更快）手写getter/setter六、Commons Codec：编解码工具6.1 Maven依赖<dependency> <groupId>commons-codec</groupId> <artifactId>commons-codec</artifactId> <version>1.16.0</version> </dependency>6.2 常用编解码import org.apache.commons.codec.binary.Base64; import org.apache.commons.codec.binary.Hex; import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.codec.net.URLCodec; // 1. Base64编解码 String encoded = Base64.encodeBase64String("Hello World".getBytes()); // "SGVsbG8gV29ybGQ=" byte[] decoded = Base64.decodeBase64(encoded); // "Hello World" // URL安全的Base64 String urlSafe = Base64.encodeBase64URLSafeString("Hello World".getBytes()); // 2. Hex编解码 String hex = Hex.encodeHexString("Hello".getBytes()); // "48656c6c6f" byte[] fromHex = Hex.decodeHex(hex); // 3. MD5/SHA摘要 String md5 = DigestUtils.md5Hex("password"); String sha1 = DigestUtils.sha1Hex("password"); String sha256 = DigestUtils.sha256Hex("password"); String sha512 = DigestUtils.sha512Hex("password"); // 文件MD5 String fileMd5 = DigestUtils.md5Hex(new FileInputStream("file.txt")); // 4. URL编解码 URLCodec urlCodec = new URLCodec(); String urlEncoded = urlCodec.encode("中文参数"); // "%E4%B8%AD%E6%96%87..." String urlDecoded = urlCodec.decode(urlEncoded);flowchart LR subgraph 编码类型选择 A[原始数据] --> B{用途?} B -->|网络传输| C[Base64] B -->|URL参数| D[URLEncode] B -->|密码存储| E[SHA256 + Salt] B -->|文件校验| F[MD5/SHA1] B -->|调试查看| G[Hex] end七、其他常用组件7.1 Commons CSV：CSV文件处理<dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-csv</artifactId> <version>1.10.0</version> </dependency>import org.apache.commons.csv.*; // 1. 读取CSV try (Reader reader = new FileReader("data.csv"); CSVParser parser = CSVFormat.DEFAULT .withFirstRecordAsHeader() .parse(reader)) { for (CSVRecord record : parser) { String name = record.get("name"); String age = record.get("age"); } } // 2. 写入CSV try (Writer writer = new FileWriter("output.csv"); CSVPrinter printer = new CSVPrinter(writer, CSVFormat.DEFAULT .withHeader("name", "age", "email"))) { printer.printRecord("Joey", 25, "joey@example.com"); printer.printRecord("Tom", 30, "tom@example.com"); }7.2 Commons Compress：压缩解压<dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-compress</artifactId> <version>1.25.0</version> </dependency>import org.apache.commons.compress.archivers.zip.*; import org.apache.commons.compress.utils.IOUtils; // 1. 创建ZIP文件 try (ZipArchiveOutputStream zos = new ZipArchiveOutputStream( new FileOutputStream("archive.zip"))) { File fileToZip = new File("document.txt"); ZipArchiveEntry entry = new ZipArchiveEntry(fileToZip, fileToZip.getName()); zos.putArchiveEntry(entry); IOUtils.copy(new FileInputStream(fileToZip), zos); zos.closeArchiveEntry(); } // 2. 解压ZIP文件 try (ZipFile zipFile = new ZipFile("archive.zip")) { Enumeration<ZipArchiveEntry> entries = zipFile.getEntries(); while (entries.hasMoreElements()) { ZipArchiveEntry entry = entries.nextElement(); File destFile = new File("output", entry.getName()); try (InputStream is = zipFile.getInputStream(entry); OutputStream os = new FileOutputStream(destFile)) { IOUtils.copy(is, os); } } }7.3 Commons Pool2：对象池<dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> <version>2.12.0</version> </dependency>sequenceDiagram participant Client as 客户端 participant Pool as 对象池 participant Factory as 对象工厂 Client->>Pool: borrowObject() alt 池中有空闲对象 Pool->>Client: 返回空闲对象 else 池中无空闲对象 Pool->>Factory: makeObject() Factory->>Pool: 新对象 Pool->>Client: 返回新对象 end Client->>Client: 使用对象 Client->>Pool: returnObject() Pool->>Pool: 对象回归池中import org.apache.commons.pool2.*; import org.apache.commons.pool2.impl.*; // 1. 定义对象工厂 public class MyConnectionFactory extends BasePooledObjectFactory<MyConnection> { @Override public MyConnection create() { return new MyConnection(); // 创建新对象 } @Override public PooledObject<MyConnection> wrap(MyConnection conn) { return new DefaultPooledObject<>(conn); } @Override public void destroyObject(PooledObject<MyConnection> p) { p.getObject().close(); // 销毁对象 } @Override public boolean validateObject(PooledObject<MyConnection> p) { return p.getObject().isValid(); // 验证对象是否有效 } } // 2. 创建对象池 GenericObjectPoolConfig<MyConnection> config = new GenericObjectPoolConfig<>(); config.setMaxTotal(10); // 最大对象数 config.setMaxIdle(5); // 最大空闲数 config.setMinIdle(2); // 最小空闲数 config.setTestOnBorrow(true); // 借出时验证 GenericObjectPool<MyConnection> pool = new GenericObjectPool<>( new MyConnectionFactory(), config ); // 3. 使用对象池 MyConnection conn = null; try { conn = pool.borrowObject(); // 借出对象 conn.doSomething(); } finally { if (conn != null) { pool.returnObject(conn); // 归还对象 } }7.4 Commons CLI：命令行解析<dependency> <groupId>commons-cli</groupId> <artifactId>commons-cli</artifactId> <version>1.6.0</version> </dependency>import org.apache.commons.cli.*; // 定义选项 Options options = new Options(); options.addOption("h", "help", false, "显示帮助信息"); options.addOption("v", "version", false, "显示版本"); options.addOption("f", "file", true, "输入文件路径"); options.addOption("o", "output", true, "输出文件路径"); // 解析命令行 CommandLineParser parser = new DefaultParser(); try { CommandLine cmd = parser.parse(options, args); if (cmd.hasOption("h")) { HelpFormatter formatter = new HelpFormatter(); formatter.printHelp("myapp", options); return; } if (cmd.hasOption("f")) { String inputFile = cmd.getOptionValue("f"); System.out.println("Input file: " + inputFile); } } catch (ParseException e) { System.err.println("解析错误: " + e.getMessage()); } // 使用示例: java -jar myapp.jar -f input.txt -o output.txt八、最佳实践与踩坑指南8.1 版本选择flowchart TB A[选择Apache Commons版本] --> B{项目JDK版本?} B -->|JDK 8+| C[使用最新稳定版] B -->|JDK 7| D[查看兼容版本] B -->|JDK 6| E[考虑升级JDK] C --> F{是否有历史依赖?} F -->|有commons-lang| G[迁移到commons-lang3] F -->|有commons-collections| H[迁移到commons-collections4] F -->|无| I[直接使用新版]8.2 常见踩坑坑1：BeanUtils.copyProperties参数顺序// ❌ 错误！很多人会搞反 BeanUtils.copyProperties(source, dest); // ✅ 正确！dest在前，source在后 BeanUtils.copyProperties(dest, source); // 💡 记忆技巧：想象成 dest = source坑2：StringUtils.isEmpty vs isBlankString str = " "; // ❌ 可能不是你想要的 if (StringUtils.isEmpty(str)) { // false！ // 不会进入 } // ✅ 通常你想要的是这个 if (StringUtils.isBlank(str)) { // true // 会进入 }坑3：commons-lang vs commons-lang3// ❌ 旧版本（已过时） import org.apache.commons.lang.StringUtils; // ✅ 新版本 import org.apache.commons.lang3.StringUtils; // 两个包不能混用！坑4：BeanUtils性能问题// ❌ 在循环中使用BeanUtils（性能差） for (UserDTO dto : dtoList) { UserEntity entity = new UserEntity(); BeanUtils.copyProperties(entity, dto); // 每次都反射 entities.add(entity); } // ✅ 使用MapStruct或手写转换 for (UserDTO dto : dtoList) { entities.add(userMapper.toEntity(dto)); // 编译期生成代码 }坑5：FileUtils.deleteDirectory的陷阱// ❌ 危险！如果路径错误可能删除重要文件 FileUtils.deleteDirectory(new File(userInputPath)); // ✅ 先验证路径 File dir = new File(userInputPath); if (dir.exists() && dir.isDirectory() && dir.getAbsolutePath().startsWith("/safe/path/")) { FileUtils.deleteDirectory(dir); }8.3 性能对比参考操作Apache Commons替代方案性能比较字符串判空StringUtils.isBlankstr == null \\str.trim().isEmpty()相当属性复制BeanUtilsMapStructMapStruct快10-100倍集合操作CollectionUtilsStream APIStream更灵活文件读取FileUtilsFiles.readString (JDK11+)相当JSON处理无Jackson/Gson使用Jackson8.4 与现代Java的关系flowchart LR subgraph Java8之前[Java 8 之前] A1[Commons Lang] --> B1[字符串/对象操作] A2[Commons Collections] --> B2[集合操作] A3[Commons IO] --> B3[文件IO] end subgraph Java8之后[Java 8+ 可替代部分] C1[Optional] --> D1[空值处理] C2[Stream API] --> D2[集合操作] C3[java.time] --> D3[日期处理] C4[Files类] --> D4[文件IO] end subgraph 仍然有用[仍然推荐使用] E1[StringUtils] --> F1[字符串判空等] E2[FileUtils] --> F2[复杂文件操作] E3[DigestUtils] --> F3[摘要计算] E4[特殊集合] --> F4[Bag/BidiMap等] end九、快速参考卡片StringUtils速查// 判空 StringUtils.isBlank(str) // null/""/空格 都返回true StringUtils.isEmpty(str) // null/"" 返回true // 默认值 StringUtils.defaultString(str) // null → "" StringUtils.defaultString(str, "default") // null → "default" StringUtils.defaultIfBlank(str, "default") // 截取 StringUtils.left(str, 5) // 左边5个字符 StringUtils.right(str, 5) // 右边5个字符 StringUtils.mid(str, 2, 5) // 从位置2开始取5个 StringUtils.substringBefore(str, ",") StringUtils.substringAfter(str, ",") // 判断 StringUtils.contains(str, "abc") StringUtils.containsIgnoreCase(str, "ABC") StringUtils.startsWith(str, "prefix") StringUtils.endsWith(str, "suffix")CollectionUtils速查// 判空 CollectionUtils.isEmpty(collection) CollectionUtils.isNotEmpty(collection) // 集合运算 CollectionUtils.union(a, b) // 并集 CollectionUtils.intersection(a, b) // 交集 CollectionUtils.subtract(a, b) // 差集 // 安全操作 CollectionUtils.emptyIfNull(collection) CollectionUtils.size(collection) // null安全FileUtils速查// 读 FileUtils.readFileToString(file, charset) FileUtils.readLines(file, charset) // 写 FileUtils.writeStringToFile(file, data, charset) FileUtils.writeLines(file, lines) // 复制 FileUtils.copyFile(src, dest) FileUtils.copyDirectory(srcDir, destDir) // 删除 FileUtils.deleteQuietly(file) FileUtils.deleteDirectory(dir) FileUtils.cleanDirectory(dir)总结Apache Commons是Java开发的得力助手，但也要注意：不要过度依赖：Java 8+已经提供了很多替代方案注意版本：使用lang3和collections4等新版本性能敏感场景：BeanUtils等基于反射的工具要谨慎使用保持更新：定期升级依赖，获取bug修复和性能优化最后的最后：工具再好，也要知道它的边界。了解原理，才能用得更好！🚀
guava是google开发的一个基础java类库，包括主要的模块为utilities/Functional/Collections/concurrency/cache/eventbus/work files，guava提供了很多好用的api，使用guava编程可以有效提高开发效率和代码质量。Joiner(list/map转化为string) private static final String COMMA = ","; private static final String EMPTY = ""; private static final String EQUALS = "="; public static String oldJoiner(List<String> dataList) { // 老的方法实现 if (dataList == null) { return null; } StringBuilder accum = new StringBuilder(); for (String item : dataList) { accum.append(item == null ? EMPTY : item).append(COMMA); } String joinResult = accum.toString(); return joinResult.substring(0, joinResult.length() - 1); } public static String oldJoiner(Map<String,String> dataMap){ if (dataMap == null){ return null; } // 老的方式，转换代码和判断比较多 StringBuilder accum = new StringBuilder(); for (Map.Entry<String,String> item : dataMap.entrySet()) { accum.append(item.getKey() == null ? EMPTY : item.getKey()).append(EQUALS).append(item.getValue()).append(COMMA); } String joinResult = accum.toString(); return joinResult.substring(0, joinResult.length() - 1); } public static String joiner(List<String> dataList) { if (dataList == null) { return null; } // guava的实现，该方法使用类似于python中的",".join(list) return Joiner.on(COMMA).useForNull(EMPTY).join(dataList); } public static String appendTo(List<String> dataList){ return Joiner.on(COMMA).useForNull(EMPTY).appendTo(new StringBuilder(), dataList).toString(); } public static String joiner(Map<String,String> dataMap){ if (dataMap == null){ return null; } // guava joiner将map值转化为string return Joiner.on(COMMA).useForNull(EMPTY).withKeyValueSeparator(EQUALS).join(dataMap); } public static void main(String[] args) { // 要求:将list转化为使用逗号分隔的字符串 List<String> dataList = Arrays.asList("1", "2", "3", null); System.out.println(appendTo(dataList)); // 测试代码 TestUtil.assertIsTrue(TestUtil.equals(joiner(dataList), oldJoiner(dataList)), "值不等"); dataList = null; TestUtil.assertIsTrue(TestUtil.equals(joiner(dataList), oldJoiner(dataList)), "值不等"); Map<String,String> dataMap = new HashMap<>(5); dataMap.put("a","1"); dataMap.put("b","2"); dataMap.put("c","3"); dataMap.put(null,"4"); dataMap.put("e","5"); TestUtil.assertIsTrue(TestUtil.equals(joiner(dataMap), oldJoiner(dataMap)), "值不等"); dataMap = null; TestUtil.assertIsTrue(TestUtil.equals(joiner(dataMap), oldJoiner(dataMap)), "值不等"); }Splitter(string转化为list/map)private static final String COMMA = ","; private static final String EMPTY = ""; private static final String EQUALS = "="; public static List<String> oldSplit(String data){ if(data == null){ return null; } String[] array = data.split(COMMA); List<String> resultList = new ArrayList<>(array.length); for(String item: array){ if(item != null && !EMPTY.equals(item.trim())){ resultList.add(item.trim()); } } return resultList; } public static List<String> split(String data){ if(data == null){ return null; } // 可以split 到Iterator/List/Stream // 分隔符可以支持正则表达式 return Splitter.on(COMMA).omitEmptyStrings().trimResults().splitToList(data); } public static Map<String,String> oldSplit2Map(String data){ if(data == null){ return null; } String[] array = data.split(COMMA); Map<String,String> resultMap = new HashMap<>(array.length); for(String item: array){ if(item != null && !EMPTY.equals(item.trim())){ String[] innerArr = item.split(EQUALS); if(innerArr.length==2) { resultMap.put(innerArr[0], innerArr[1]); } } } return resultMap; } public static Map<String,String> split2Map(String data){ if(data == null){ return null; } // 和joiner相同，可以split 到Map(Beta版本) return Splitter.on(COMMA).omitEmptyStrings().trimResults().withKeyValueSeparator(EQUALS).split(data); } public static void main(String[] args) { String data = "1,2 , 3, "; TestUtil.assertIsTrue(TestUtil.equals(split(data), oldSplit(data)), "值不等"); TestUtil.assertIsTrue(TestUtil.equals(split(null), oldSplit(null)), "值不等"); String data2 = "=4,a=1,b=2,c=3,e=5"; TestUtil.assertIsTrue(TestUtil.equals(split2Map(data2), split2Map(data2)), "值不等"); TestUtil.assertIsTrue(TestUtil.equals(split2Map(null), split2Map(null)), "值不等"); }Collection(封装) public static List<String> oldCreateList(String a, String b, String c){ // m1: java入门的方法 // List<String> resultList = new ArrayList<>(); // resultList.add(a); // resultList.add(b); // resultList.add(c); // return resultList; // m2: 创建了一个list-Arrays$ArrayList,仅能读取，不支持add/remove操作(java.lang.UnsupportedOperationException) // return Arrays.asList(a,b,c); // m3: 包了一层ArrayList，可add/remove，但性能有降低，且很难看 return new ArrayList<>(Arrays.asList(a,b,c)); } public static List<String> createList(String a, String b, String c){ // 创建一个array list return Lists.newArrayList(a,b,c); } public static List<List<String>> partition(List<String> sourceList, int size){ // 将list按size分块，apache同样提供了ListUtils.partition方法 return Lists.partition(sourceList, size); } public static List<Integer> oldTransform(List<String> sourceList){ if(sourceList==null){ return null; } // 原生转换 return sourceList.stream().filter(Objects::nonNull).map(String::length).collect(Collectors.toList()); } public static List<Integer> transform(List<String> sourceList){ if(sourceList==null){ return null; } // Lists转换，比起原生代码略短 // 没有找到过滤方法 return Lists.transform(sourceList, s->s==null?null:s.length()); } public static Map createMap(){ //指定的是实际容量，而非capacity，由于loadFactor默认为0.75，所以new HashMap(16)的实际容量只有12，超出会自动扩容影响效率 return Maps.newHashMapWithExpectedSize(16); } public static void main(String[] args){ String a="1",b="2",c="3"; List<String> oldList = oldCreateList(a, b, c); TestUtil.assertIsTrue(TestUtil.equals(oldCreateList(a, b, c), createList(a,b,c)), "值不等"); oldList.add("12"); System.out.println(partition(oldList, 3)); TestUtil.assertIsTrue(TestUtil.equals(oldTransform(oldList),transform(oldList)),"值不等"); TestUtil.assertIsTrue(TestUtil.equals(oldTransform(null),transform(null)),"值不等"); oldList.add(null); TestUtil.assertIsTrue(TestUtil.equals(oldTransform(oldList),transform(oldList)),"值不等"); }### 本地缓存() 我们一般会将频繁读取的数据保存在redis等缓存服务中，在每次请求的时候，优先从缓存中读取以减小数据开销，提升性能 * 在访问远程缓存的情况下，一般耗时为3-5ms，虽然这个值不大，但是在一个流程中频繁读取的情况下，也会拖慢系统响应速度， * 此时我们需要使用本地缓存以提升性能，本地缓存顾名思义即数据存储在本地内存中，适用于频繁读取，并且数据量不太大的场景，在应用重启时本地缓存会全部丢失。 * 而guava提供了一个易用的本地缓存 // fixme:此处为方便测试，设置5秒钟后缓存过期 private static final String CACHE_CONFIG = "initialCapacity=50,maximumSize=1000,expireAfterWrite=5s"; /** * 以下为guava cache最简单的使用方法 * cachebuilder.from 更便于参数化控制缓存效果 */ private LoadingCache<String, Optional<Integer>> cacheLoader = CacheBuilder.from(CACHE_CONFIG) .build(new CacheLoader<String, Optional<Integer>>() { @Override public Optional<Integer> load(String key) { /** * 在此处初始化缓存，从配置,redis或db从读取 * 备:使用optional的原因是load无法处理空值 */ return Optional.ofNullable(initCache(key)); } }); public Integer initCache(String key) { // fixme:假设加载缓存需要1秒钟 Uninterruptibles.sleepUninterruptibly(1, TimeUnit.SECONDS); return new Random().nextInt(); } public static void main(String[] args) throws ExecutionException, InterruptedException { Stopwatch stopwatch = Stopwatch.createStarted(); String key1 = "paramA"; Caches caches = new Caches(); Optional<Integer> val = caches.cacheLoader.get(key1); System.out.println(String.format("key:%s,val:%s,time cost:%s", key1, val.get(), stopwatch.elapsed(TimeUnit.MILLISECONDS))); stopwatch.reset(); stopwatch.start(); val = caches.cacheLoader.get(key1); // 此处读取本地缓存，加载时间为0 System.out.println(String.format("key:%s,val:%s,time cost:%s", key1, val.get(), stopwatch.elapsed(TimeUnit.MILLISECONDS))); stopwatch.reset(); stopwatch.start(); String key2 = "paramB"; val = caches.cacheLoader.get(key2); System.out.println(String.format("key:%s,val:%s,time cost:%s", key2, val.get(), stopwatch.elapsed(TimeUnit.MILLISECONDS))); stopwatch.reset(); // Uninterruptibles.sleepUninterruptibly(5, TimeUnit.SECONDS); // 手动清除本地缓存 caches.cacheLoader.invalidateAll(); System.out.println("size:"+caches.cacheLoader.asMap().size()); stopwatch.start(); val = caches.cacheLoader.get(key1); // 此时耗时为1秒 System.out.println(String.format("key:%s,val:%s,time cost:%s", key1, val.get(), stopwatch.elapsed(TimeUnit.MILLISECONDS))); }基础类型基础类型的封装:Booleans/Bytes/Chars/Doubles/Floats/Ints/Longs/Shorts八种基本类型的封装和衍生类型 * 原生类型数组转化为list * 校验:checkedCast,ensureCapacity * 比较:compare,min,max * 查找:contains,indexOf,lastIndexOf * 类型转换:toByteArray,fromByteArray,fromBytes,stringConverter,toArray,asList,tryParse * 合并:concat,join, * 排序:sortDescending,reverse