尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
装饰模式实战:用组合替代继承实现职责分离
1. 项目概述为什么装饰模式是“单一职责”的第一块试金石你有没有遇到过这样的场景一个类刚写完功能跑得挺顺结果产品经理第二天就甩来三条需求——要加日志、要支持缓存、还要做权限校验。你盯着那个已经三百行的UserService类手指悬在键盘上犹豫着该往getUserById()方法里塞第几个if (isDebug)还是干脆把整个类复制一份改名叫UserServiceWithCacheAndAuth这种“越改越胖、越修越乱”的感觉就是设计模式还没真正长进肌肉记忆里的典型信号。装饰模式Decorator Pattern不是教你怎么写更炫的代码而是帮你守住一条底线每个类只负责一件事且这件事必须足够小、足够纯粹。它不反对功能叠加但坚决反对把日志、缓存、鉴权这些横切关注点硬塞进业务主干里。就像给一杯咖啡加奶泡、加焦糖、加肉桂粉——你可以一层层往上叠但每一层都独立封装、可插可拔底层那杯浓缩咖啡本身永远只管“萃取咖啡因”这一件事。这个模式被冠以“单一职责模式一”不是因为它最早出现而是因为它最直观、最无痛地展示了“职责分离”如何落地。它不依赖抽象工厂不绕弯子搞模板方法就靠一个接口、一个基础实现、一组装饰器三样东西搭起一座可扩展的桥。我带过的几个刚转岗的后端同学第一次用装饰模式重构掉 47 行重复的日志代码后当场在 Slack 里发了个“原来解耦真的能让人呼吸顺畅”的表情包——这比任何 UML 图都说明问题。它适合谁不是只适合架构师画蓝图而是适合每一个每天和if-else、try-catch、log.info()打交道的普通开发者。只要你写过需要“临时增强”某个对象行为的代码比如给 HTTP 客户端加重试、给数据库连接加超时、给文件读取加进度回调你就已经在装饰模式的射程之内。它不挑语言Java 的InputStream体系、Python 的decorator语法糖、Go 的组合嵌入底层逻辑一脉相承。接下来我们就从一张白纸开始不抄框架源码不堆概念术语只用最朴素的 Java 示例把装饰模式怎么想、怎么写、怎么避坑掰开揉碎讲清楚。2. 核心设计思路拆解为什么不用继承为什么不用代理2.1 继承的陷阱静态绑定与爆炸式类增长初学者最容易想到的方案是继承。比如有个Coffee接口public interface Coffee { String getDescription(); double getCost(); }基础实现是Espressopublic class Espresso implements Coffee { Override public String getDescription() { return Espresso; } Override public double getCost() { return 1.99; } }现在要加牛奶Milk、摩卡Mocha、奶泡WhippedCream三种配料。如果用继承就得写出EspressoWithMilkEspressoWithMochaEspressoWithWhippedCreamEspressoWithMilkAndMochaEspressoWithMilkAndWhippedCreamEspressoWithMochaAndWhippedCreamEspressoWithMilkAndMochaAndWhippedCream这还只是 3 种配料、1 种基底。现实中一个电商商品服务可能有库存校验、价格计算、优惠券叠加、物流预估、风控拦截等 8 个横切逻辑。按继承算光组合数就是 2⁸ 256 个子类。更致命的是这些类一旦生成就无法在运行时动态开关。用户下单时临时要求“跳过风控”你得改代码、发版本、重启服务——这在高可用系统里是不可接受的。提示继承解决的是“是什么”is-a的问题比如Dog是Animal而装饰模式解决的是“有什么能力”has-a behavior的问题比如Coffee“有加奶的能力”。混淆这两者是设计腐化的起点。2.2 代理模式的局限侵入性与透明性缺失另一个常见思路是代理Proxy。写一个CoffeeProxy内部持有一个Coffee实例在调用前后加逻辑public class CoffeeProxy implements Coffee { private final Coffee coffee; private final boolean needLog; public CoffeeProxy(Coffee coffee, boolean needLog) { this.coffee coffee; this.needLog needLog; } Override public String getDescription() { if (needLog) System.out.println(Logging getDescription); return coffee.getDescription(); } Override public double getCost() { if (needLog) System.out.println(Logging getCost); return coffee.getCost(); } }问题立刻浮现配置爆炸每加一个新能力缓存、权限就要改CoffeeProxy的构造函数和所有方法违背开闭原则职责不清CoffeeProxy变成一个“万能胶水”既管日志又管缓存还管异常自己成了新的上帝类透明性差调用方必须知道这是个代理得显式传入needLogtrue这样的标志位破坏了接口的纯粹性。装饰模式的精妙之处正在于它用“组合”替代“继承”用“同接口”替代“代理壳”。它的核心契约只有三条装饰器和被装饰对象实现同一个接口保证类型兼容装饰器内部持有被装饰对象的引用保证行为委托装饰器在调用被装饰对象方法的前、后、或周围插入自己的逻辑保证增强可控。这三条规则像三把尺子量出了所有“伪装饰”的破绽。我见过最典型的反模式是有人写了个LoggingDecorator但它不实现Coffee接口而是实现了一个LoggingAware新接口——这就彻底脱离了装饰模式的语境变成自创协议后续根本没法和CacheDecorator套在一起用。2.3 装饰模式的三层结构接口、核心、装饰器回到Coffee场景标准装饰模式结构如下第一步定义统一契约接口public interface Coffee { String getDescription(); // 描述用于展示 double getCost(); // 价格用于结算 }注意这里不叫Beverage或Drink而叫Coffee。命名要具体、有业务语义避免过度抽象。接口方法必须是业务核心行为不能塞setLogger()这种管理方法。第二步实现最小可行核心Concrete Componentpublic class Espresso implements Coffee { Override public String getDescription() { return Espresso; } Override public double getCost() { return 1.99; } } public class HouseBlend implements Coffee { Override public String getDescription() { return House Blend; } Override public double getCost() { return 0.89; } }关键点核心类不关心任何装饰也不持有任何装饰器引用。它只做最原始的事。就像一个刚出厂的手机没有贴膜、没装壳、没下载 App但能正常开机打电话。第三步构建可叠加的装饰器Concrete Decorator// 所有装饰器的基类可选但强烈推荐 public abstract class CoffeeDecorator implements Coffee { protected final Coffee coffee; // 必须 final保证不可变性 public CoffeeDecorator(Coffee coffee) { this.coffee coffee; } Override public String getDescription() { return coffee.getDescription(); // 默认委托子类可覆盖 } Override public double getCost() { return coffee.getCost(); // 默认委托子类可覆盖 } } // 具体装饰器加牛奶 public class Milk extends CoffeeDecorator { public Milk(Coffee coffee) { super(coffee); } Override public String getDescription() { return coffee.getDescription() , Milk; } Override public double getCost() { return coffee.getCost() 0.10; } } // 具体装饰器加摩卡 public class Mocha extends CoffeeDecorator { public Mocha(Coffee coffee) { super(coffee); } Override public String getDescription() { return coffee.getDescription() , Mocha; } Override public double getCost() { return coffee.getCost() 0.20; } }看到这里你应该能感受到结构的轻盈感Milk和Mocha彼此完全解耦互不知晓对方存在它们只依赖Coffee接口不依赖Espresso或HouseBlend的具体实现新增WhippedCream装饰器只需写一个新类不碰任何已有代码运行时组合new Mocha(new Milk(new Espresso()))顺序即执行顺序清晰可读。这个结构之所以成立本质是利用了面向接口编程和组合优于继承两大基石。它不追求“一次设计永久适用”而是追求“每次增强成本最低”。这才是工程实践中真正可持续的优雅。3. 核心细节解析与实操要点从接口定义到线程安全3.1 接口设计的三个生死线很多团队在落地装饰模式时第一步就栽在接口定义上。不是太宽就是太窄或者埋了雷。以下是我在多个项目中踩出的血泪经验生死线一方法必须是“业务动词”禁用 setter/getter 风格错误示范public interface Coffee { void setLogger(Logger logger); // ❌ 管理方法混入业务接口 void enableCache(boolean enable); String getName(); double getPrice(); }问题setLogger把装饰器的配置逻辑强加给所有实现类getName/getPrice这种 getter 名称模糊无法体现业务意图。正确做法是回归业务本质public interface Coffee { String getDescription(); // “描述”是用户看到的文案有明确业务含义 double getCost(); // “成本”是结算核心直接关联钱 void brew(); // “冲泡”是动作代表执行业务主流程 }brew()方法看似多余但在某些场景如异步处理、状态机中它是触发装饰链执行的关键入口。生死线二禁止在接口中暴露“装饰器专用方法”错误示范public interface Coffee { String getDescription(); double getCost(); // ❌ 下面两个方法让接口承担了装饰器的职责 void addDecorator(Decorator decorator); ListDecorator getDecorators(); }后果所有Coffee实现类包括Espresso都得实现这些与自身无关的方法违反单一职责。装饰器的组装逻辑应该放在创建阶段Builder 模式或使用阶段工厂方法绝不污染核心接口。生死线三考虑是否需要“装饰器元信息”有些复杂场景需要知道当前对象被哪些装饰器包裹。比如风控系统要记录“本次请求经过了日志→缓存→权限→限流四层校验”。这时可以在装饰器基类中加一个getDecoratorName()方法public abstract class CoffeeDecorator implements Coffee { protected final Coffee coffee; private final String decoratorName; public CoffeeDecorator(Coffee coffee, String decoratorName) { this.coffee coffee; this.decoratorName decoratorName; } public String getDecoratorName() { return decoratorName; } // ... 其他方法 }这样new Mocha(new Milk(new Espresso()))就能通过getDecoratorName()逐层获取Mocha → Milk → Espresso。但要注意这只是调试和监控用绝不用于业务逻辑分支判断比如if (name.equals(Mocha)) doX()否则又回到了条件判断的老路。3.2 装饰器的构造与生命周期管理装饰器不是工具类它是有状态的对象。它的构造方式直接决定了系统的可维护性。方式一构造函数注入推荐public class CacheDecorator extends CoffeeDecorator { private final CacheManager cacheManager; public CacheDecorator(Coffee coffee, CacheManager cacheManager) { super(coffee, Cache); this.cacheManager cacheManager; } }优点依赖明确、不可变、易于单元测试MockcacheManager即可。缺点如果依赖太多比如同时要CacheManager、RedisTemplate、MetricsRegistry构造函数会很长。此时应引入 Builder 模式public class CacheDecoratorBuilder { private Coffee coffee; private CacheManager cacheManager; private String cacheKeyPrefix coffee:; public CacheDecoratorBuilder coffee(Coffee coffee) { this.coffee coffee; return this; } public CacheDecoratorBuilder cacheManager(CacheManager cacheManager) { this.cacheManager cacheManager; return this; } public CacheDecorator build() { return new CacheDecorator(coffee, cacheManager, cacheKeyPrefix); } } // 使用 Coffee coffee new CacheDecoratorBuilder() .coffee(new Espresso()) .cacheManager(redisCache) .build();方式二Spring Bean 自动装配企业级项目首选在 Spring 环境下装饰器可以声明为Component利用Autowired注入依赖Component public class LoggingDecorator extends CoffeeDecorator { private final Logger logger; public LoggingDecorator(Qualifier(businessLogger) Logger logger) { super(null, Logging); // 注意此处 coffee 为 null需在 postConstruct 中设置 this.logger logger; } PostConstruct public void init() { // 此处无法注入 coffee因为 coffee 本身也是 Bean循环依赖 // 正确做法用 ObjectProvider 延迟获取 } }但这里有个经典陷阱循环依赖。LoggingDecorator需要Coffee而Coffee的某个实现如EspressoService可能也依赖LoggingDecorator。解决方案是使用ObjectProviderComponent public class LoggingDecorator extends CoffeeDecorator { private final Logger logger; private final ObjectProviderCoffee coffeeProvider; // 延迟获取 public LoggingDecorator(Qualifier(businessLogger) Logger logger, ObjectProviderCoffee coffeeProvider) { super(null, Logging); this.logger logger; this.coffeeProvider coffeeProvider; } Override public String getDescription() { Coffee coffee coffeeProvider.getObject(); // 运行时才获取 logger.info(Getting description for: {}, coffee.getDescription()); return coffee.getDescription(); } }这种方式牺牲了一点性能每次调用都 getObject但换来了彻底的解耦和可测试性。方式三函数式装饰Java 8轻量级场景如果装饰逻辑简单比如只是加一行日志可以用 Lambda 包装public class CoffeeUtils { public static Coffee withLogging(Coffee coffee, Logger logger) { return new CoffeeDecorator(coffee, Logging) { Override public String getDescription() { logger.info(getDescription called); return coffee.getDescription(); } Override public double getCost() { logger.info(getCost called); return coffee.getCost(); } }; } } // 使用 Coffee coffee CoffeeUtils.withLogging( new Espresso(), LoggerFactory.getLogger(CoffeeLogger) );优势零配置、无类文件、适合 PoC 或脚本场景。劣势无法序列化、难以调试、不支持复杂状态管理。我的建议是超过 3 行逻辑就写正式装饰器类。3.3 线程安全与状态管理装饰器不是无状态的一个常见误解是“装饰器只是包装肯定线程安全”。错。装饰器可以持有状态而状态就是并发的雷区。看这个反例public class CounterDecorator extends CoffeeDecorator { private int callCount 0; // ❌ 实例变量多线程共享 public CounterDecorator(Coffee coffee) { super(coffee, Counter); } Override public String getDescription() { callCount; // 多线程下 不是原子操作 return coffee.getDescription() (called callCount times); } }当 100 个请求并发调用getDescription()最终callCount可能远小于 100。修复方案有三方案一用AtomicInteger推荐private final AtomicInteger callCount new AtomicInteger(0); Override public String getDescription() { int current callCount.incrementAndGet(); // 原子操作 return coffee.getDescription() (called current times); }方案二用ThreadLocal适合线程内计数private final ThreadLocalInteger threadLocalCount ThreadLocal.withInitial(() - 0); Override public String getDescription() { int current threadLocalCount.get() 1; threadLocalCount.set(current); return coffee.getDescription() (thread-local count: current ); }注意ThreadLocal必须在请求结束时remove()否则 Web 容器线程复用会导致内存泄漏。Spring 的RequestContextHolder就是这么做的。方案三彻底无状态最安全如果计数只是为了监控直接上报到 Metrics 系统装饰器自身不保存状态public class MetricsDecorator extends CoffeeDecorator { private final MeterRegistry meterRegistry; public MetricsDecorator(Coffee coffee, MeterRegistry meterRegistry) { super(coffee, Metrics); this.meterRegistry meterRegistry; } Override public String getDescription() { // 上报指标不保存本地状态 meterRegistry.counter(coffee.description.calls).increment(); return coffee.getDescription(); } }我的实操心得是装饰器的状态只应服务于其装饰目的本身。日志装饰器可以有Logger引用缓存装饰器可以有CacheManager引用但绝不该有ListString recentRequests这种业务无关的缓存。状态越少越可靠。4. 实操过程与核心环节实现从咖啡店到电商订单系统4.1 完整可运行示例咖啡店收银系统我们用一个完整、可编译运行的 Java 示例把前面所有要点串起来。这不是玩具代码而是真实项目中简化出的骨架。Step 1定义核心接口与基础实现// src/main/java/com/example/coffee/Coffee.java public interface Coffee { String getDescription(); double getCost(); void brew(); // 新增模拟实际执行 } // src/main/java/com/example/coffee/Espresso.java public class Espresso implements Coffee { Override public String getDescription() { return Espresso; } Override public double getCost() { return 1.99; } Override public void brew() { System.out.println(Brewing Espresso...); } }Step 2构建装饰器基类与具体装饰器// src/main/java/com/example/coffee/CoffeeDecorator.java public abstract class CoffeeDecorator implements Coffee { protected final Coffee coffee; private final String decoratorName; public CoffeeDecorator(Coffee coffee, String decoratorName) { this.coffee coffee; this.decoratorName decoratorName; } Override public String getDescription() { return coffee.getDescription(); } Override public double getCost() { return coffee.getCost(); } Override public void brew() { coffee.brew(); } public String getDecoratorName() { return decoratorName; } } // src/main/java/com/example/coffee/Milk.java public class Milk extends CoffeeDecorator { public Milk(Coffee coffee) { super(coffee, Milk); } Override public String getDescription() { return coffee.getDescription() , Milk; } Override public double getCost() { return coffee.getCost() 0.10; } } // src/main/java/com/example/coffee/WhippedCream.java public class WhippedCream extends CoffeeDecorator { public WhippedCream(Coffee coffee) { super(coffee, WhippedCream); } Override public String getDescription() { return coffee.getDescription() , WhippedCream; } Override public double getCost() { return coffee.getCost() 0.25; } }Step 3编写启动类验证效果// src/main/java/com/example/coffee/Main.java public class Main { public static void main(String[] args) { // 基础咖啡 Coffee espresso new Espresso(); System.out.println(espresso.getDescription() - $ espresso.getCost()); // 输出Espresso - $1.99 // 加牛奶 Coffee milkEspresso new Milk(new Espresso()); System.out.println(milkEspresso.getDescription() - $ milkEspresso.getCost()); // 输出Espresso, Milk - $2.09 // 加牛奶和奶泡注意顺序先加奶泡再加牛奶描述顺序会不同 Coffee fancyCoffee new Milk(new WhippedCream(new Espresso())); System.out.println(fancyCoffee.getDescription() - $ fancyCoffee.getCost()); // 输出Espresso, WhippedCream, Milk - $2.34 // 验证装饰器链可追溯 if (fancyCoffee instanceof CoffeeDecorator) { CoffeeDecorator decorator (CoffeeDecorator) fancyCoffee; System.out.println(Top decorator: decorator.getDecoratorName()); // Milk // 递归获取所有装饰器名实际项目中可用栈或队列 } } }运行结果清晰印证了装饰模式的核心价值getDescription()和getCost()的计算是层层叠加的不是覆盖new Milk(new WhippedCream(new Espresso()))的构造顺序直接决定了getDescription()中字符串的拼接顺序每个装饰器只关心自己那一层的逻辑Milk不知道WhippedCream存在WhippedCream也不关心Espresso的具体实现。这个例子虽小但已具备生产环境所需的全部要素接口隔离、组合扩展、运行时动态、职责清晰。4.2 迁移到真实场景电商订单服务的装饰链把咖啡店升级为电商订单系统装饰模式的价值会指数级放大。假设我们有一个OrderService核心接口是public interface OrderService { Order createOrder(CreateOrderRequest request); Order getOrderById(String orderId); void cancelOrder(String orderId); }现在要叠加以下能力日志记录LogDecorator缓存CacheDecorator权限校验AuthDecorator事务管理TransactionDecorator链路追踪TraceDecoratorStep 1定义装饰器基类带通用能力public abstract class OrderServiceDecorator implements OrderService { protected final OrderService orderService; protected final Tracer tracer; // 全局追踪器 protected final MeterRegistry metrics; // 全局指标注册器 public OrderServiceDecorator(OrderService orderService, Tracer tracer, MeterRegistry metrics) { this.orderService orderService; this.tracer tracer; this.metrics metrics; } // 通用方法记录耗时指标 protected T T timed(String operation, SupplierT supplier) { long start System.nanoTime(); try { T result supplier.get(); metrics.timer(order. operation .duration).record(System.nanoTime() - start, TimeUnit.NANOSECONDS); return result; } catch (Exception e) { metrics.counter(order. operation .error).increment(); throw e; } } }Step 2实现具体装饰器以权限校验为例public class AuthDecorator extends OrderServiceDecorator { private final AuthService authService; public AuthDecorator(OrderService orderService, Tracer tracer, MeterRegistry metrics, AuthService authService) { super(orderService, tracer, metrics); this.authService authService; } Override public Order createOrder(CreateOrderRequest request) { // 在调用前校验 authService.checkPermission(request.getUserId(), CREATE_ORDER); return timed(createOrder, () - orderService.createOrder(request)); } Override public Order getOrderById(String orderId) { // 获取订单ID对应的用户ID可能需要查DB此处简化 String userId extractUserIdFromOrderId(orderId); authService.checkPermission(userId, READ_ORDER); return timed(getOrderById, () - orderService.getOrderById(orderId)); } Override public void cancelOrder(String orderId) { String userId extractUserIdFromOrderId(orderId); authService.checkPermission(userId, CANCEL_ORDER); timed(cancelOrder, () - orderService.cancelOrder(orderId)); } private String extractUserIdFromOrderId(String orderId) { // 实际中可能从 Redis 或 DB 查询此处返回占位符 return user_123; } }Step 3组装装饰链Spring Boot 配置Configuration public class OrderServiceConfig { Bean Primary public OrderService orderService( Qualifier(baseOrderService) OrderService baseService, Tracer tracer, MeterRegistry metrics, AuthService authService, CacheManager cacheManager, TransactionTemplate transactionTemplate) { // 从内到外组装最内层是基础服务最外层是最后执行的装饰器 OrderService service baseService; service new TraceDecorator(service, tracer); service new MetricsDecorator(service, metrics); service new CacheDecorator(service, cacheManager); service new AuthDecorator(service, tracer, metrics, authService); service new TransactionDecorator(service, transactionTemplate); return service; } Bean Qualifier(baseOrderService) public OrderService baseOrderService() { return new JpaOrderService(); // 真实的 JPA 实现 } }关键点解析组装顺序决定执行顺序new TransactionDecorator(...)在最外层意味着事务管理是最高层切面它包裹了所有其他逻辑每个装饰器只做一件事AuthDecorator只管权限CacheDecorator只管缓存TraceDecorator只管打 trace ID指标和追踪是通用能力timed()方法在基类中定义所有子类可复用避免重复代码Primary和Qualifier确保 Spring 正确注入业务代码中Autowired OrderService自动拿到装饰链无需感知内部结构。我参与过的一个千万级订单系统就是用这套模式将原本 2000 行的OrderServiceImpl拆成 7 个职责单一的装饰器。上线后新增一个“风控拦截”装饰器只用了 2 小时而旧系统加同样功能要改 3 个类、写 5 个单元测试、协调 2 个团队联调。4.3 参数配置与性能调优别让装饰器拖垮系统装饰模式不是银弹滥用会导致性能雪崩。以下是必须关注的参数和调优点参数一缓存装饰器的 key 生成策略错误做法用request.toString()作为缓存 key。问题toString()可能包含时间戳、随机 UUID 等变化字段导致缓存命中率为 0。正确做法定义CacheKeyGenerator接口由业务方实现public interface CacheKeyGeneratorT { String generateKey(T request); } // 订单缓存 key 生成器 public class OrderCacheKeyGenerator implements CacheKeyGeneratorCreateOrderRequest { Override public String generateKey(CreateOrderRequest request) { return String.format(order:create:%s:%s, request.getUserId(), DigestUtils.md5Hex(request.getItems().toString())); // 只对稳定字段哈希 } }参数二日志装饰器的采样率控制全量日志会压垮磁盘和 ELK。必须支持动态采样public class SamplingLogDecorator extends OrderServiceDecorator { private final Logger logger; private final double sampleRate; // 0.0 ~ 1.0 public SamplingLogDecorator(OrderService orderService, Tracer tracer, MeterRegistry metrics, Logger logger, Value(${log.sample-rate:0.01}) double sampleRate) { super(orderService, tracer, metrics); this.logger logger; this.sampleRate sampleRate; } private boolean shouldLog() { return Math.random() sampleRate; // 简单随机采样 } Override public Order createOrder(CreateOrderRequest request) { if (shouldLog()) { logger.info(Creating order for user: {}, items: {}, request.getUserId(), request.getItems()); } return orderService.createOrder(request); } }参数三装饰链深度限制防递归极端情况下装饰器可能意外形成循环引用如 A 装饰 BB 又装饰 A。可在基类中加深度检查public abstract class OrderServiceDecorator implements OrderService { protected final OrderService orderService; private final int depth; // 当前装饰深度 public OrderServiceDecorator(OrderService orderService, int depth) { this.orderService orderService; this.depth depth; if (depth 10) { // 深度阈值 throw new IllegalStateException(Decorator chain too deep: depth); } } // 构造时传递 depth 1 protected OrderServiceDecorator(OrderService orderService) { this(orderService, 1); } }性能实测数据某电商系统装饰器类型单次调用平均耗时QPS 下降幅度对比无装饰日志采样 1%0.2ms 0.1%缓存Redis-15ms缓存命中 / 2ms未命中35%整体权限校验本地缓存0.5ms 0.3%全链路追踪0.1ms 0.05%5 层装饰链总开销1.1ms0.5%结论合理设计的装饰链性能损耗几乎可忽略。真正的瓶颈永远在业务逻辑本身而不是装饰模式。5. 常见问题与排查技巧实录那些文档里不会写的坑5.1 问题速查表高频故障与根因分析问题现象可能根因排查命令/技巧解决方案NullPointerException在装饰器coffee字段coffee构造时传入null且未做空检查在装饰器基类构造函数中加Objects.requireNonNull(coffee)所有装饰器构造函数强制非空校验缓存装饰器不生效总是穿透到 DBCacheKeyGenerator返回的 key 包含动态字段如new Date()打印key值观察是否每次请求都不同用Data的EqualsAndHashCode注解只对稳定字段生成 hash日志装饰器输出重复日志同一请求打 3 次装饰链中存在多个日志装饰器或 Spring AOP 与装饰器混用grep -r logger.info src/查找所有日志点统一用Primary装饰器禁用 AOP 切面getCost()计算结果错误比预期多 0.1装饰器getCost()方法中错误地写了return coffee.getCost() 0.10 0.10重复加价在Milk装饰器中加断点观察coffee.getCost()返回值所有装饰器的getCost()必须只加自己那一份费用不叠加其他装饰器应用启动失败报BeanCurrentlyInCreationExceptionOrderService循环依赖A 装饰器依赖 BB 又依赖 A./gradlew --scan查看 bean 创建图改用ObjectProvider延迟获取或拆分OrderService为QueryService和CommandService5.2 独家避坑技巧来自生产环境的 5 条铁律铁律一装饰器必须可序列化否则分布式系统必跪如果你的系统用 Redis 做二级缓存或者用 Kafka 传消息装饰器对象很可能被序列化。而CoffeeDecorator持有的Logger、Tracer等都是不可序列化的。解决方案所有不可序列化字段加transient关键字重写readObject()方法在反序列化后重新注入依赖private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException { stream.defaultReadObject(); // 重新获取 Spring 上下文中的 Bean this.logger ApplicationContextProvider.getBean(Logger.class); }注意ApplicationContextProvider是一个静态持有ApplicationContext的工具类需在 Spring 启动时初始化。
RELATED

相关推荐

银河麒麟下SVN源码编译安装及配置实战指南

银河麒麟下SVN源码编译安装及配置实战指南

简介:面向在银河麒麟操作系统中自建SVN版本控制服务的中高级运维人员与项目管理员,这份资料以Word文档形式提供完整的源码安装流程。正文以subversion-1.8.14、apr-1.6.5、apr-util-1.6.1、sqlite-amalgamation-3071501的版本选择为起点,先交…

📅 2026/10/12 11:38:40
初创企业建站选型实测:BBWEYY、Codex+亚马逊AWS、比文云与Dreamweaver的低成本验证与上线速度对比(含TaoToken统一Key接入)

初创企业建站选型实测:BBWEYY、Codex+亚马逊AWS、比文云与Dreamweaver的低成本验证与上线速度对比(含TaoToken统一Key接入)

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/10/12 11:33:35
AI日报制作全流程:从信息采集到深度解读的实操指南

AI日报制作全流程:从信息采集到深度解读的实操指南

1. 一份AI日报的诞生逻辑每天早上八点半,我会准时打开自己的信息聚合面板,把过去24小时里散落在各个角落的AI动态过一遍。这个习惯坚持了快两年,起因很简单——信息太碎了,碎到你刷一上午手机,可能只记住了三条真正有用…

📅 2026/10/12 11:33:35
MORE NEWS

更多资讯

📰

Nginx双机主备高可用:Keepalived+VIP漂移实战与避坑指南

1. 为什么单台Nginx扛不住,双机主备到底在解决什么问题很多团队在项目初期都习惯把Nginx往一台机器上一装,配好反向代理和负载均衡就完事了。流量小的时候确实没问题,单机跑个几千QPS跟玩似的。但一旦业务上了规模,或者对可用性有…

📰

跨时钟域亚稳态详解:从D触发器原理到同步器设计与MTBF量化

先讲一个我调板时遇到的场景:某块主控板上的FPGA和外部ADC通过各自独立的晶振时钟工作,接口信号在逻辑分析仪上一切正常,仿真波形也干干净净,但设备连续跑了一天之后,系统偶尔会出现一次“莫名其妙”的寄存器跳变&…

📰

CAN与CAN-FD总线深度解析:从物理层到工程实战的硬实时通信之道

1. 为什么我说CAN总线是"沙子到车辙"系列里最扎实的一章"从沙子到车辙"这个系列写到第4章,前面几章我聊了芯片怎么从硅砂变成逻辑门、软件怎么从汇编跑成实时任务,但说实话,真正让我觉得"这玩意儿总算能跟机械硬碰硬…

📰

ARM Cortex-A9体系结构深度解析:工作状态、运行模式与存储格式

1. 从一颗芯片说起:为什么ARM Cortex-A9至今仍是嵌入式学习的绝佳样本如果你翻过任何一本讲嵌入式体系结构的教材,或者拆解过任何一款2010年前后的主流消费电子产品,大概率会跟ARM Cortex-A9这个名字打上照面。它不算新,甚至可以说…

📰

空中机器人入门:四旋翼原理与旋转矩阵学习笔记

直接写正文。两周刷完Aerial Robotics前两章,趁着热乎劲把课程笔记和作业里踩过的坑一起整理出来。这门课是空中机器人领域的入门基石,前两周的内容设计得很聚焦:第一周带你把四旋翼的机械结构和飞行原理吃透,第二周开始上数学工具…

📰

AI芯片软硬件协同设计实战:从指令集到编译器的全链路优化

1. 从一颗芯片的诞生说起:为什么软硬件必须一起设计一颗AI芯片从概念到量产,中间要经过架构设计、RTL实现、物理设计、流片、封装、驱动开发、编译器适配、框架对接等十几个大环节。很多刚入行的朋友会下意识地把这些环节分成两拨:硬件工程师…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬