Java 后端 Service 方法扩展点设计
Java 后端 Service 方法扩展点设计
当我们的
Service方法需要在"执行前 / 执行中 / 执行后 / 执行失败"四个节点预埋入口,让后续业务方能"只重写对应方法就实现新需求"时,该怎么设计?
这是一道非常典型的"对扩展开放、对修改封闭"(OCP)诉求题。在 Spring + MyBatis-Plus 的项目里,由于 ServiceImpl 的单继承约束,可选的方案并不多。本文给出一份可直接落地的方案 A(模板方法 + ServiceImpl 继承),以及完整的最佳实践清单。
一、问题的本质
业务开发里,我们经常遇到这样的场景:
典型场景
createOrder在主流程外需要加一个"下单前限流"逻辑- 订单创建成功后,需要再"记录审计日志"
- 支付失败时,需要"回滚库存"或"发送告警"
- 不同业务线(普通订单 / 秒杀订单)需要走不同的业务流程
共同特征:主流程不变,但每个具体业务要插入不同的逻辑。
我们想要的,是这样的扩展能力:
[Before] → [主流程] → [After]
↓ (失败)
[OnFailure]后续业务方只需要"重写某一段"就能完成新需求,不必修改原有方法。
二、4 种方案横向对比
业界常见的 4 种方案各有适用场景:
| 方案 | 侵入性 | 灵活度 | 是否能改主流程 | 适用场景 |
|---|---|---|---|---|
| A. 模板方法 | 中 | 中 | ✅ | 框架 / 库内部,或能控制继承层级的业务项目 |
| B. 策略 + Hook 接口 | 中 | 高 | ✅ | 业务级可插拔,Service 已被三方库继承时 |
| C. Spring AOP | 极低 | 高 | ⚠️ | 横切关注点(日志 / 权限 / 事务) |
| D. ApplicationEvent | 低 | 中 | ❌ | 事后通知 / 异步解耦 |
为什么不是方案 B
策略 + Hook 接口虽然更灵活,但要求 Service 内部手动调度 hook 列表。对于一个已经写完、迭代过 N 个版本的 Service 层,把 hook 调用"植入"到每个方法里,工作量大且容易漏。
模板方法方案把 hook 调用收敛在抽象基类里,新加 Service 时天然就带上了扩展点 —— 这是它在 MyBatis-Plus 场景下最大的优势。
三、推荐方案 A:模板方法 + ServiceImpl 继承
3.1 核心思路
把模板方法下移一层:让模板类自己继承
ServiceImpl<M, T>,业务类继承模板类,从而绕过 Java 单继承限制。
ServiceImpl<M, T> ← MyBatis-Plus 提供(基类)
↑
AbstractXxxService ← 我们写的"模板类",定义扩展点
↑
XxxServiceImpl ← 业务类,继承模板 + 实现业务方法3.2 完整实现
Step 1:定义抽象模板类
/**
* 订单 Service 抽象模板
*
* <p>继承 ServiceImpl 保留 MyBatis-Plus 的 CRUD 能力,
* 同时通过模板方法模式暴露生命周期扩展点。
*/
public abstract class AbstractOrderServiceImpl
extends ServiceImpl<OrderMapper, Order> {
/** 模板方法:final 锁定流程,不允许子类重写整体结构 */
public final Order createOrder(OrderRequest req) {
beforeCreate(req); // 钩子 1
validate(req);
Order order = doCreate(req); // 抽象方法(业务核心)
this.save(order); // 直接用继承的 save
afterCreate(order); // 钩子 2
return order;
}
// ====== 钩子方法(默认空实现,子类按需重写)======
/** 执行前钩子 */
protected void beforeCreate(OrderRequest req) { }
/** 成功执行后钩子 */
protected void afterCreate(Order order) { }
/** 失败钩子 */
protected void onCreateFailure(OrderRequest req, Throwable e) { }
/** 抽象方法:业务核心逻辑,必须由子类实现 */
protected abstract Order doCreate(OrderRequest req);
}模板方法三件套的语义
final模板方法:禁止子类重写整个流程,保证主流程稳定protected钩子方法:默认空实现,子类可选择性重写abstract业务方法:强制子类实现核心逻辑
这种"漏斗型继承层级"是模板方法的标准形态。
Step 2:业务类继承模板
/** 普通订单实现 */
@Service
public class NormalOrderServiceImpl extends AbstractOrderServiceImpl {
@Override
protected Order doCreate(OrderRequest req) {
// 普通下单逻辑
return new Order()
.setUserId(req.getUserId())
.setItems(req.getItems())
.setStatus(OrderStatus.CREATED);
}
}/** 秒杀订单实现:演示多钩子组合 */
@Service
public class FlashSaleOrderServiceImpl extends AbstractOrderServiceImpl {
@Override
protected void beforeCreate(OrderRequest req) {
// 1. 秒杀前置:限流 + 资格校验
rateLimiter.acquire(req.getUserId());
seckillService.checkQualification(req);
}
@Override
protected Order doCreate(OrderRequest req) {
// 2. 秒杀专属:走独立下单引擎
return flashSaleEngine.submit(req);
}
@Override
protected void afterCreate(Order order) {
// 3. 秒杀后置:异步发短信
smsService.sendAsync(order.getUserId(), "秒杀成功");
}
@Override
protected void onCreateFailure(OrderRequest req, Throwable e) {
// 4. 失败回滚:释放预占库存
inventory.release(req.getItems());
log.warn("秒杀下单失败,userId={},err={}", req.getUserId(), e.getMessage());
}
}看到没?业务方从来没有动过 AbstractOrderServiceImpl,只通过重写钩子就完成了"普通 → 秒杀"的扩展。
四、必须配套的 4 个最佳实践
光有模板方法不够,下面 4 件事不做,扩展点会从"利器"变成"坑"。
4.1 钩子异常隔离
高频踩坑
单个钩子失败绝不能拖垮主流程。比如审计 hook 写 DB 挂了,不能让订单创建也跟着失败。
错误写法:
public final Order createOrder(OrderRequest req) {
beforeCreate(req); // 如果 beforeCreate 抛异常,主流程直接挂
Order order = doCreate(req);
this.save(order);
afterCreate(order);
return order;
}正确写法:在模板里加一个"安全包装":
public final Order createOrder(OrderRequest req) {
safeBefore(req);
try {
Order order = doCreate(req);
this.save(order);
safeAfter(order);
return order;
} catch (Throwable e) {
safeOnFailure(req, e);
throw e;
}
}
private void safeBefore(OrderRequest req) {
try { beforeCreate(req); }
catch (Exception e) { log.error("[Hook] before 失败", e); }
}
private void safeAfter(Order order) {
try { afterCreate(order); }
catch (Exception e) { log.error("[Hook] after 失败", e); }
}
private void safeOnFailure(OrderRequest req, Throwable e) {
try { onCreateFailure(req, e); }
catch (Exception ex) { log.error("[Hook] onFailure 失败", ex); }
}关键原则
钩子方法的异常只能记录日志、不能抛出。主流程的异常必须抛出(否则调用方不知道失败了)。safeXxx 包装类就是这个边界的强制约束。
4.2 钩子可观测
没观测的扩展点 = 没法排障的黑盒。每个钩子必须有执行耗时和成败状态的埋点。
private void safeBefore(OrderRequest req) {
Timer.Sample sample = Timer.start(meterRegistry); // Micrometer
String hookName = "beforeCreate:" + this.getClass().getSimpleName();
try {
beforeCreate(req);
meterRegistry.counter("service.hook.success", "name", hookName).increment();
} catch (Exception e) {
meterRegistry.counter("service.hook.fail", "name", hookName).increment();
log.error("[Hook] {} 失败", hookName, e);
} finally {
sample.stop(Timer.builder("service.hook.cost")
.tag("name", hookName)
.register(meterRegistry));
}
}接入 Prometheus 后,你就能看到这样的图:
service_hook_cost_seconds{name="beforeCreate:FlashSaleOrderServiceImpl",quantile="0.99"} 0.023
service_hook_success_total{name="afterCreate:FlashSaleOrderServiceImpl"} 12834
service_hook_fail_total{name="onCreateFailure:FlashSaleOrderServiceImpl"} 42线上排障时一眼就能定位是哪个钩子慢、哪个钩子失败率高。
4.3 钩子接口稳定性
钩子参数不要"加字段就 breaking"
模板方法的钩子签名一旦定下来,就是公开 API。后续给钩子加参数 = 改方法签名 = 所有子类编译失败。
反例:
// v1 版本
protected void beforeCreate(OrderRequest req) { }
// 业务方 1:实现了 v1
@Override
protected void beforeCreate(OrderRequest req) { ... }
// v2 想加个上下文参数
protected void beforeCreate(OrderRequest req, HookContext ctx) { } // 业务方 1 全部编译失败 💥正例:用 Context 模式传递数据,参数永远稳定:
// 钩子参数固定不变
protected void beforeCreate(HookContext ctx) { }
// 扩展数据都走 ctx.attributes
public class HookContext {
private final OrderRequest request;
private final Map<String, Object> attributes = new HashMap<>();
public <T> T get(String key) { return (T) attributes.get(key); }
public void put(String key, Object value) { attributes.put(key, value); }
}业务方需要新数据?自己往 ctx 里 put 就行,钩子签名永远不动。
4.4 钩子顺序控制
当一个 Service 上挂了多个钩子(比如"埋点 hook + 审计 hook + 业务 hook"),先后顺序很重要。
如果用模板方法(方案 A),每个 Service 只有一个钩子集,没有顺序问题。但如果业务方想叠加更多横切钩子,建议方案 C(AOP)配合 @Order:
@Aspect
@Component
public class OrderAuditAspect {
@Order(10) // 数字越小,优先级越高
@Around("@annotation(auditable)")
public Object around(ProceedingJoinPoint pjp, Auditable auditable) {
// 审计逻辑
}
}模板方法管"业务扩展点",AOP 管"横切钩子" —— 两者各司其职,组合使用。
五、常见陷阱清单
| # | 陷阱 | 后果 | 正确做法 |
|---|---|---|---|
| 1 | 给所有方法都加钩子 | 80% 钩子永远不被调用,增加阅读成本 | 只在核心业务节点(create / update / submit)加 |
| 2 | 钩子签名频繁变动 | 子类全部编译失败 | 用 Context 模式,参数永远稳定 |
| 3 | 钩子抛异常不隔离 | 审计挂了 → 业务也挂 | safeXxx 三件套 |
| 4 | 钩子无埋点 | 线上慢 / 失败无法定位 | Micrometer 统一埋点 |
| 5 | 钩子里反向调 Service | 循环依赖 → 启动失败 | 钩子只做"前置 / 后置",不要调 Service 自身方法 |
| 6 | 抽象方法太多 | 子类实现成本高 | 一个模板方法只配 1 个抽象方法 + 多个钩子 |
| 7 | 模板类直接被 @Autowired | 抽象类不能实例化,启动报错 | 注入具体子类,或用 @Qualifier |
六、与其他方案的关系
方案 A 并不是"银弹"。在一个真实项目里,应该是多种方案组合使用:
┌────────────────────────────────────────────────────┐
│ 业务核心流程的扩展点(createOrder / pay / ship) │ ← 方案 A:模板方法
├────────────────────────────────────────────────────┤
│ 跨业务的横切关注点(审计 / 鉴权 / 慢 SQL 监控) │ ← 方案 C:Spring AOP
├────────────────────────────────────────────────────┤
│ 事后解耦通知(订单创建后通知库存 / 物流) │ ← 方案 D:ApplicationEvent
└────────────────────────────────────────────────────┘判断标准:
- 你能控制继承层级吗?能 → 方案 A
- 跨多个 Service 都要加同一个能力?→ 方案 C(AOP)
- 扩展只关心"事后通知"不关心主流程?→ 方案 D(Event)
- Service 已被三方类库强占继承位?→ 方案 B(策略 + Hook)
七、落地 Checklist
在团队里推广这套方案前,建议先对照检查一遍:
八、总结
全文总结
在 MyBatis-Plus 项目里做 Service 扩展点,方案 A(模板方法 + ServiceImpl 继承) 是最贴合现实的方案:改动小、模板语义纯、IDE 友好、与 MBP 生态零冲突。
配合 异常隔离 + 可观测 + Context 模式 + AOP 补充,就能搭出一套对扩展开放、对修改封闭的可维护扩展点体系。
业务核心流程用模板方法埋点,横切关注点交给 AOP,事后解耦用 Event。组合使用,才是工程化的扩展点设计。