1. 为什么说Spring Boot本身就是设计模式的活教材不少开发者对设计模式的态度很分裂面试前背得滚瓜烂熟写业务代码时又想不起来用还有人觉得Spring Boot是个重框架设计模式是学院派的东西两者没什么关系。我做了这么多年Spring Boot项目反而越来越觉得——Spring Boot之所以能让你写出看起来很优雅的代码本质上就是它内部大量运用了经典设计模式并且把这些模式的使用方式焊死在了框架习惯里。你天天用Spring其实天天都在用设计模式只是很多时候没意识到而已。这篇文章我想把选出来的九个经典模式放到Spring Boot的真实机制里去拆不是从类图讲起而是先看框架在哪个环节用了它再看你平时怎么在业务代码里复用它。这里选的九个模式不是凑数而是覆盖了Spring Boot项目从启动到请求处理到业务解耦的全程分别有单例模式、工厂模式、模板方法模式、策略模式、观察者模式、装饰器模式、适配器模式、建造者模式、代理模式。它们不是孤立存在的而是像齿轮一样互相咬合理解了这一层其实你对Spring Boot本身的理解也会更透。我见过太多人在项目里随手new一个Service类然后启动就报Bean相关错误也见过有人用Autowired注入一个类但完全不清楚这个类默认是单例的结果在类里随手写了个带状态的成员变量上线之后出现诡异的数据串号问题。这些问题说到底是没把设计模式对应到Spring Boot的运行机制上。所以这篇博客不装高深就把九个模式一个个掰开讲清楚它在框架里落在哪、你该怎么在自己代码里用、有哪些坑我在实战里踩过。2. 单例模式与工厂模式Spring容器最底层的两条命脉2.1 Spring Boot中单例模式的三层体现先看一段最简单的代码Service public class UserService { public void doSomething() { System.out.println(handle user); } }你写了一个标注Service的类Spring在启动时会扫描它默认把这个Bean创建成单例也就是整个应用生命周期内只有这一个实例。为什么Spring要做单例最直接的原因是性能——如果一个Service被注入到几十个Controller和其他Service里每次都创建新实例对象创建开销、GC压力、内存占用都会成倍增长。尤其是无状态服务类它们只包含业务方法和依赖根本没必要创建多份。但这个单例有两个容易踩坑的点。第一是有状态成员变量。由于是单例所有请求共享同一个实例如果我在类里写了一个private int count 0;然后某个方法里count就会出现不同请求之间数据互相污染的问题。我在实际项目里就经历过一个用户请求修改了缓存状态另一个用户马上看到了脏数据排查了很久最后定位到是单例Bean里写了非线程安全的成员变量。正确的做法是单例Service里只放无状态逻辑或者依赖本身就是ThreadLocal、并发安全组件需要保存请求数据时放到方法参数或ThreadLocal里。第二点是关于单例Bean依赖原型Bean的问题。Spring里还有一个Scope(prototype)注解表示每次获取都创建新实例但如果你在单例Bean里注入一个原型Bean会发现注入进来的原型Bean实际上也被单例化了。因为注入动作只发生一次。想解决这个问题用ObjectFactoryT或者Lookup注解。很多人不知道这个细节导致原型Bean永远不生效这也是单例模式下最常见的一类隐蔽Bug。顺便说一下Spring Boot的Configuration类本身也是在Scope(singleton)下工作的但它通过动态代理保证每个Bean方法在同一个配置类里被多次调用时返回的还是同一个实例这个机制是CGLIB代理在背后起作用这里已经开始有代理模式的影子了后面我还会详细讲。2.2 BeanFactory经典工厂模式在Spring中的具体实现单例模式只解决了实例共享的问题那Spring是怎么管理这些复杂创建逻辑的答案就是工厂模式。Java原生创建对象用new而Spring的BeanFactory是一个大工厂它根据配置信息XML、注解、Java Config决定创建什么类型、怎么装配依赖、什么时候创建。public interface BeanFactory { Object getBean(String name) throws BeansException; T T getBean(ClassT requiredType) throws BeansException; }如果你把这个接口理解为生产Bean的工厂那ApplicationContext就是这个工厂的进一步升级版——在工厂基础上加了事件发布、国际化、资源加载、环境解析等能力。我们在写业务代码时接触到的ApplicationContext.getBean()本质就是在调用工厂的获取产品接口具体创建什么产品、产品的生命周期怎么管全部由工厂内部实现决定调用方根本不用关心。这就引出一个很多人没想明白的问题为什么要用工厂方式创建对象而不是直接new举个具体例子我做一个支付项目时有微信支付、支付宝、银联三种支付方式每种支付的初始化参数和逻辑都不一样。如果业务代码里直接new那每处都要写一堆初始化细节而且要随着支付渠道增加不断改改改。用工厂把创建逻辑集中到工厂内部业务层只需要getBean(wechatPayService)这样的调用即可。更关键的是工厂能控制对象的生命周期作用域——单例、原型、请求作用域、会话作用域这是裸new完全做不到的。不过在Spring Boot里我建议你尽量别手动getBean除非是在极端场景下比如动态注册Bean、运行时按条件获取实现类因为这样会绕开IDE的依赖注入分析和编译期检查代码也更难测试。工厂模式在Spring Boot里的正确打开方式是用Configuration Bean按条件组装然后用Autowired注入让容器帮你完成工厂调用这个动作。实践要点Bean方法返回值是接口类型而非实现类调用方依赖接口扩展实现类时替换方法内部逻辑即可。防坑提示getBean手动获取不要出现在Service代码里否则耦合度极高测试时也麻烦。2.3 工厂单例模式联手实现优雅的对象获取方式这两个模式在Spring里往往是组合出现的。最常见的一个组合点就是Spring Boot的自动配置AutoConfiguration。比如你引入spring-boot-starter-data-redisSpring Boot会根据条件ConditionalOnClass、ConditionalOnProperty等自动创建RedisConnectionFactory、RedisTemplate这些Bean创建的Bean默认也是单例装载到容器里供全局注入。这就是工厂负责按需创建单例负责全局复用的典型合作案例。我在做微服务项目时经常用工厂单例模式来统一管理第三方SDK客户端。举个真实设计例子Configuration public class SmsSenderFactoryConfig { Bean ConditionalOnProperty(name sms.provider, havingValue aliyun) public SmsSender aliyunSmsSender() { return new AliyunSmsSender(new AliyunConfig(...)); } Bean ConditionalOnProperty(name sms.provider, havingValue tencent) public SmsSender tencentSmsSender() { return new TencentSmsSender(new TencentConfig(...)); } }之后业务代码里Autowired对应名字的Bean或者注入MapString, SmsSender整个报文发送模块切换云厂商不用改任何业务代码只改配置项。这就是模式组合的价值——单例保证资源不重复创建工厂保证创建逻辑集中可控。如果你只懂一个模式是很难达到这种灵活性的。3. 策略模式与模板方法模式消除if-else与流程固定化的两把刀3.1 策略模式在Spring Boot中的正确落地方式我第一次意识到策略模式重要是在一个被if-else支配的后台项目里。那个项目有十多种营销活动每种活动都有完全不同的价格计算逻辑。核心方法长这样public BigDecimal calculatePrice(String activityType, Order order) { if (fullReduction.equals(activityType)) { // 满减计算 } else if (discount.equals(activityType)) { // 折扣计算 } else if (coupon.equals(activityType)) { // 优惠券计算 } // 每次加活动都要卷进这个巨型方法 }这个代码的致命弱点有两个一是可读性灾难谁也不敢动这个大泥球方法二是开闭原则完全被打破加一个活动就要改一次核心计算方法而且极容易影响已有活动的逻辑。用策略模式重构后的做法是定义一个策略接口每种计价逻辑对应一个实现类用Spring管理这些实现类然后在运行时选择具体策略。public interface PriceStrategy { String getActivityType(); BigDecimal calculate(Order order); } Component public class FullReductionStrategy implements PriceStrategy { Override public String getActivityType() { return fullReduction; } Override public BigDecimal calculate(Order order) { // 满减逻辑 } } Component public class DiscountStrategy implements PriceStrategy { Override public String getActivityType() { return discount; } Override public BigDecimal calculate(Order order) { // 折扣逻辑 } }然后利用Spring在注入时支持按接口收集所有实现类并且能用Map按名称索引这个特性来做策略分发Service public class PriceCalculator { private final MapString, PriceStrategy strategyMap; public PriceCalculator(ListPriceStrategy strategies) { this.strategyMap strategies.stream() .collect(Collectors.toMap(PriceStrategy::getActivityType, Function.identity())); } public BigDecimal calculate(String activityType, Order order) { PriceStrategy strategy strategyMap.get(activityType); if (strategy null) { throw new IllegalArgumentException(Unsupported activity type: activityType); } return strategy.calculate(order); } }注入一个ListPriceStrategySpring自动把所有实现类放进去再转成Map以后新增活动只需要加一个实现类核心计价器完全不用动。这是我目前在项目里最推荐的策略模式落地方案完全没有一点花架子全部是实用代码。3.2 策略模式在业务场景中的取舍心得用了几年策略模式我总结出一些取舍心得。首先策略模式不是用来消灭所有if-else的。如果只有两三种情况且每种逻辑就一两行硬套策略模式反而增加类数量代码更碎。我的判断标准是当分支逻辑超过三个、且每个分支的复杂度和变化频率都较高时策略模式才真正划算。另外策略实现类之间的关联复杂性也要注意比如有些策略需要组合执行那单纯用Map选择就有局限需要引入策略编排器相当于一个简单的Chain模式。还有一些实际项目经验用MapString, PriceStrategy做策略选择时如果Spring容器里有大量策略类这个Map本身健康吗其实要小心策略类名字冲突getActivityType()返回相同值一旦两个实现类返回同一个类型字符串Collectors.toMap会直接抛IllegalStateException。所以我在团队里定的规则是所有策略实现类的type必须全局唯一并且写单元测试做校验用反射扫描所有实现类发现重复type立即失败。这个测试成本极低但能避免上线前才撞车的尴尬。3.3 模板方法模式的典型应用场景模板方法模式的核心思想是父类定义好流程骨架某些步骤延迟到子类实现。业务上最典型的场景就是那些大部分逻辑相同只有局部不同的任务流。Spring的JdbcTemplate就是模板方法的最好代表——它封装了数据库操作的标准流程获取连接、准备语句、执行查询、处理结果集、释放资源。这些步骤是一步步固定的骨架但最后如何把ResultSet转成业务对象这部分又通过回调接口RowMapper交给调用者实现。所以你写jdbcTemplate.query(sql, (rs, rowNum) - new User(...))时其实就是在模板框架里填入自己的定制逻辑。实际操作中我自己写过一个数据导入任务的模板方法案例public abstract class AbstractDataImportJob { public final void execute() { ListString lines readFile(); ListString validLines filterInvalidLines(lines); for (String line : validLines) { ImportResult result doImport(line); handleResult(result); } sendSummaryReport(); } protected abstract ListString readFile(); protected abstract ImportResult doImport(String line); protected abstract void handleResult(ImportResult result); // 模板方法里的通用逻辑filterInvalidLines, sendSummaryReport 已实现 }订单批量同步、商品Excel导入、会员标签计算……不同业务各自继承这个抽象类只实现读文件和单条导入这些变化部分而过滤、汇总、通知这些固定流程子类想破坏都破坏不了。模板方法模式还能配合protected钩子方法比如if (isSupportNotify()) displayNotify()让子类选择性覆盖某些默认行为。这里要注意的踩坑点是模板方法设计时别把该父类封闭的骨架搞成public。骨架方法用final修饰这样子类才不能重写整套流程不然模板就失去了意义。我见过好几个人把execute()写成非final结果某个子类为了省事重写了整个流程之后公共逻辑更新那个子类行为全变了排查起来特别崩溃。4. 观察者模式与建造者模式从事件驱动到对象构建的实战表达4.1 Spring的事件机制就是观察者模式的框架级实现观察者模式的核心是状态变化通知依赖方。在Spring里这一模式被实现为内置的事件机制包含三个核心组件ApplicationEvent事件载体、ApplicationListener或EventListener注解方法观察者、ApplicationEventPublisher事件发布者。我举一个业务例子用户下单成功后往往要触发短信通知、库存扣减、积分增加、优惠券状态更新、订单日志记录等多个动作。如果全部同步写在下单服务里下单方法会越来越长而且任何一个通知服务出问题都可能让下单失败被回滚或者因为不得不处理异常而让核心链路代码臃肿。用事件机制重构后下单服务只做一件事保存订单、发布事件。EventListener public void onOrderCreated(OrderCreatedEvent event) { sendSms(event.getOrder()); } EventListener public void onOrderCreated(OrderCreatedEvent event) { inventoryService.deduct(event.getOrder()); }发布事件的代码applicationEventPublisher.publishEvent(new OrderCreatedEvent(order));多个监听器相互独立、互不干扰其中一个失败了只要异常处理得当不会导致下单主流程回滚。这就是观察者模式给代码带来的模块化自由度。不过要注意默认情况下Spring事件监听器是同步执行的。也就是说发布事件的线程会阻塞直到所有监听器执行完。如果你的监听器比较耗时例如发短信、调外部接口会直接影响主流程响应时间。解决办法有两个一是监听器方法上加Async注解配合启动类上的EnableAsync启用异步二是自建线程池在监听器内部手动提交异步任务。但异步化之后检查链路难度加大网络超时、线程池拒绝的问题需要额外关注。我会在事件里带上一个traceId用于链路追踪这个习惯帮了我很多次。4.2 观察者模式下事件治理的几个经验事件机制虽好用多了也会产生问题。第一个问题就是事件满天飞项目里上百个事件类有些人甚至用事件处理本来同步调用就该做的事情导致代码的调用链断裂看代码的人必须到处找EventListener才能拼出完整流程。应对办法是事件只用于真正的跨模块协作或异步解耦场景模块内部的直接调用还是老老实实写在Service代码里。第二个问题就是事件监听的异常处理。同步事件中如果某个监听器抛出异常Spring默认会中断事件分发后续监听器不会被执行。这是个容易踩坑的点。比如下单事件有三个监听器发短信、扣库存、写日志如果发短信的监听器抛异常扣库存和写日志都没了。解决办法可以是监听器各自try-catch兜底用日志记录错误或者在发布事件时用Executor异步分发每个任务独立执行。我偏向于前者——监听器边界范围内吃掉业务异常但要注意给监控系统上报不能静默吞掉。4.3 建造者模式在代码和框架中的双重化身建造者模式Builder Pattern解决的是复杂对象构造问题核心是链式设置参数然后一次构造。它在Spring Boot里最明显的身影是RestTemplateBuilder、SecurityFilterChain的构建过程以及日常开发中我们构建复杂请求参数时的Builder模式。举个例子我在做OpenFeign调用的请求头定制时经常要构造很复杂的请求对象String token jwtService.buildToken() .setUserId(userId) .setExpireTime(LocalDateTime.now().plusHours(2)) .setRoles(roles) .setExtraData(extraMap) .sign();如果没有建造者模式这样的对象构造通常需要写一个很长很长的构造函数参数顺序一旦对错代码里全是魔法传参维护成本极高。而建造者模式通过链式调用每个set方法只对应一个参数可读性直接提升一个档次。我在真实的Spring Boot项目里用得比较多的还有基于静态工厂方法加Builder的设计比如定义一个返回Builder的create()方法public class QueryCondition { private final String keyword; private final int pageSize; private final String sortBy; public static Builder builder() { return new Builder(); } public static class Builder { private String keyword; private int pageSize 10; private String sortBy createTime; public Builder keyword(String keyword) { this.keyword keyword; return this; } public Builder pageSize(int pageSize) { this.pageSize pageSize; return this; } public Builder sortBy(String sortBy) { this.sortBy sortBy; return this; } public QueryCondition build() { return new QueryCondition(keyword, pageSize, sortBy); } } }很多团队项目中查询条件参数越来越多就用这种方式来封装调用处一目了然。有一点需要提Lombok的Builder注解很方便但和Spring Boot某些场景配合要注意。比如JPA实体上用Builder加全参构造器时经常忘了加NoArgsConstructor和AllArgsConstructor导致Jackson反序列化需要无参构造器时报错。我在项目里经历过两次这种低级错误排查时发现报错信息指向构造器最后都是补注解解决。还有在继承关系中Lombok的Builder默认不会处理父类字段要加SuperBuilder才行这也是一个典型细节。5. 适配器模式与装饰器模式兼容旧世界和扩展新行为的两个角度5.1 Spring MVC中适配器模式的经典落地适配器模式的本质是把一个类的接口转换成客户端期望的另一个接口。Spring MVC是整个Spring生态里适配器模式最密集的地方其中HandlerAdapter是典型。想想看Spring MVC中存在多种处理器Controller方法、HttpRequestHandler、Servlet、RouterFunction……客户端DispatcherServlet期望统一调某个方法处理请求但每种处理器的调用方式不一样。这时候HandlerAdapter扮演了适配器角色public interface HandlerAdapter { boolean supports(Object handler); ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception; }每种处理器配一个对应的AdapterRequestMappingHandlerAdapter处理RequestMapping方法HttpRequestHandlerAdapter处理HttpRequestHandlerSimpleServletHandlerAdapter处理Servlet。DispatcherServlet只认HandlerAdapter一个接口具体怎么适配不同handler是各个Adapter内部的事情。业务代码里我也经常用适配器模式封装第三方SDK。比如你有自己的一套PayService接口public interface PayService { PayResult pay(PayRequest request); PayResult refund(RefundRequest request); }现在要接入一个新的支付渠道它的SDK是LegacyPayClient方法名和参数完全对不上。直接用LegacyPayClient会让业务层被第三方SDK污染。正确做法是写一个适配器Component public class LegacyPayAdapter implements PayService { private final LegacyPayClient legacyPayClient; Override public PayResult pay(PayRequest request) { LegacyRequest legacyRequest convertToLegacy(request); LegacyResponse legacyResponse legacyPayClient.submitLegacyPayment(legacyRequest); return convertFromLegacy(legacyResponse); } // refund 同样转换 }业务层完全不知道LegacyPayClient是否存在。以后切换SDK版本只需要改这个适配器类。这种模式特别适合旧系统改造、第三方服务能力下沉场景我在网关对接、航班接口对接、短信平台切换上都用过收益非常明显。5.2 装饰器模式在Spring Boot中的痕迹装饰器模式的关键在于动态地给对象增加职责而不修改其类定义。它和继承的区别是继承在编译期就确定了行为装饰器则在运行时一层层包裹灵活组合。Spring Boot里最典型的是HttpServletRequestWrapper的实现。比如我们要给所有请求增加一个统一的处理逻辑——记录请求耗时、添加公共Header、包装Request输入流来支持重复读取Body最干净的方式就是写一个装饰器类public class RepeatableReadRequestWrapper extends HttpServletRequestWrapper { private final byte[] body; public RepeatableReadRequestWrapper(HttpServletRequest request) throws IOException { super(request); this.body IOUtils.toByteArray(request.getInputStream()); } Override public ServletInputStream getInputStream() { ByteArrayInputStream byteArrayInputStream new ByteArrayInputStream(body); return new ServletInputStream() { Override public boolean isFinished() { return byteArrayInputStream.available() 0; } Override public boolean isReady() { return true; } Override public void setReadListener(ReadListener readListener) {} Override public int read() { return byteArrayInputStream.read(); } }; } }然后通过Filter把原始的Request替换成装饰器版本Component public class RequestWrapperFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) { HttpServletRequest httpServletRequest (HttpServletRequest) request; RepeatableReadRequestWrapper wrapper new RepeatableReadRequestWrapper(httpServletRequest); chain.doFilter(wrapper, response); } }这样下游的Controller、拦截器都可以多次读取请求体比如网关验签、日志记录、AOP参数打印都需要请求体但ServletRequest的InputStream默认只能读一次。装饰器模式完美解决这个需求不用改动任何业务Controller代码只加一层过滤包装。这类场景最典型的使用边界就是用装饰器增强既有组件的横切能力。但要注意装饰器模式不要在业务Service层随手滥用因为它动态组合之后调用链会比较难跟踪出了问题调试成本高。更适合用AOP本质上是代理模式处理那些横切逻辑而装饰器更专注于流对象、包装Request这类需要改变对象接口行为的场景。5.3 代理模式——Spring AOP和声明式事务背后的真相如果只能选一个模式作为Spring框架的杀手锏我选代理模式。Spring AOP、声明式事务Transactional、懒加载代理、Configuration类的CGLIB代理等全部依赖代理机制。代理模式解决的核心问题是在不修改目标对象源码的情况下给它增加额外逻辑。Spring AOP的典型场景打印日志、方法耗时统计、权限校验、缓存、事务管理。我拿Transactional举例。你写Transactional public void transfer(String fromAccount, String toAccount, BigDecimal amount) { accountDao.deduct(fromAccount, amount); accountDao.add(toAccount, amount); }Spring启动时会对transfer所在的Bean创建代理对象。当外部调用transfer时真正执行的其实不是你的原始方法而是代理里的拦截逻辑先开启事务然后调用原始方法方法正常结束则提交事务抛异常则回滚事务。这就是为什么Transactional在某些情况下会失效——比如同类内部方法调用时this.transfer()调用走的是原始对象而非代理对象事务注解根本没生效。我在项目里就排查过两次这种问题用户写了一个方法挂在UserService里内部直接this.updateAccount()注解写了等于没写。使用Spring AOP时记住一个细节Spring Boot 2.x以后默认使用CGLIB代理也就是基于继承创建子类代理所以不要再用「类需要实现接口才能被代理」的老黄历了。但也不代表一切都可以代理比如final方法无法被代理覆盖static方法无法被代理private方法也无法被代理。有些团队喜欢把所有方法都加Transactional这会让代理变得笨重而且有事可能导致代理嵌套自调用问题。我会在设计阶段就区分出哪些方法真正需要事务边界。6. 九种模式如何组合成一套完整的业务能力模型6.1 一个贴近实战的组合案例拆解理解了每个模式单点之后更重要的能力是组合使用。我拿一个典型的订单服务作为例子演示这些模式怎么在同一个模块里协同工作工厂模式OrderFactory根据订单类型创建不同订单实体普通订单、秒杀订单、预售订单。建造者模式构建复杂订单请求参数比如渠道来源、商品明细、收货信息、优惠明细用Builder流式封装。策略模式价格计算组件根据orderType路由到不同计价策略。模板方法模式订单处理主流程校验、冻结库存、扣款、发事件……定义在抽象父类里子类只实现扣款方式。观察者模式订单状态变更后发布OrderCreatedEvent让积分模块、短信模块、推荐模块异步响应。装饰器模式订单导出功能在基础导出器上动态叠加隐藏敏感信息和加水印两个装饰器。适配器模式对接第三方物流平台时用LogisticsAdapter将各家物流SDK统一成自家接口。单例模式所有无状态Service默认单例全局复用。代理模式对订单方法进行操作审计和事务控制全部由Spring AOP代理完成业务类里零侵入。当我画出这个组合图景时很多读者就会明白设计模式从来不是某个类长什么样而是你在搭建系统时每个职责边界应该怎么划分。每个模式解决一类特定的结构性问题它们组合起来才能构建一个可维护、可扩展、可测试的业务系统。6.2 模式识别能力的训练方法很多人学完设计模式后用不上根本原因是没有建立问题-模式的映射能力。我的经验是从问题气味入手而非从模式定义入手当代码中出现大量if-else按类型分支且分支逻辑复杂——考虑策略模式。当需要统一规范流程且流程固定骨架易变步骤——考虑模板方法。当模块之间需要解耦且不希望A模块感知B模块——考虑事件/观察者。当构造对象参数过多且部分可选参数组合多——考虑建造者。当需要一个全局共享的有状态服务或者资源池——考虑单例工厂组合。当需要在方法前后无侵入加逻辑——考虑代理模式。这里还得提醒一句模式不是越多越好。我在一些项目里看到有人为了交作业式地用模式每写一个Service就套一个接口实现类结果接口几十个方法就一个代码跳来跳去。架构设计的核心是平衡——在复杂度增长到一定程度时引入模式解决实际问题而不是为了看起来专业制造不必要的抽象。6.3 从Spring Boot源码的角度找模式的足迹如果想让自己的模式理解更上一层楼我的建议是抱着Spring Boot的源码去找模式的指纹。看BeanFactory的时候思考工厂模式是怎么管理对象生命周期的看DefaultListableBeanFactory时思考单例注册表结构看DispatcherServlet时思考HandlerMapping和HandlerAdapter这套策略和适配器机制看ApplicationContext.publishEvent时思考观察者的分发逻辑看CglibAopProxy时思考代理链的织入方式。这种从源码验证模式的方式远比背类图有效因为你会真实感受到设计模式不是在作业里出现而是在你依赖的最战功赫赫的基础设施里发挥威力。我自己每次技术分享都会让团队画一下某个Spring组件涉及的模式然后对比源码确认。这个过程能快速提高团队整体的代码品位比贴一堆UML图有意义多了。7. 我在九个模式实际运用中踩过的坑和换来的教训7.1 单例与有状态变量的结合是最常见的定时炸弹前面已经提到了单例Bean中有状态成员的坑这里再展开说一个具体案例。有次线上订单号生成策略从数据库序列表切换成Redis自增我在一个单例Service里加了一个成员变量做计数器缓存Service public class OrderNoGenerator { private long currentValue; // 单例变量非线程安全 public long generate() { return currentValue; } }压测时发现偶发重复订单号原因就是并发请求共享了同一个currentValue。后来改成用AtomicLong配合Redis分布式锁才解决。这个教训让我意识到单例模式下所有成员变量都是全局共享资源必须评估并发安全性。如果你在无状态Service里确实需要保存临时数据请放到ThreadLocal或方法参数里。7.2 策略模式Map注入时的BeanName冲突另一个印象深刻的坑是Map注入策略实现类时Spring提供了按BeanName注入Map的能力Autowired private MapString, FooStrategy strategyMap; // key 默认是 beanName这个用法很方便但要注意Spring BeanName默认是类名首字母小写所以Map的key是fullReductionStrategy而不是fullReduction。如果期望用自定义key就不能直接用Map注入要像前面那样在构造器里收ListFooStrategy然后自己toMap。两者混用时容易产生混乱我在代码审查时经常看到有人把BeanName和策略类型name混成一团。策略模式的另一个常见问题是策略类爆炸。十种计价策略就有十个策略类每个类如果只有几行逻辑代码仓库会多出一大批文件管理起来也很累。这时候考虑把多个相近策略合并成一个类用枚举或者配置区分行为或者考虑策略类内部再使用简单工厂方法。模式的核心是变化隔离不是类数量最大化。7.3 观察者模式下的回滚问题很多人在事件监听器里做了抛异常会影响主流程这样的处理因为这个坑让我吃过大亏。那次业务背景是下单后要发优惠券优惠券模块监听订单事件。优惠券服务外部接口超时抛出了一个运行时异常因为是同步监听下单事务直接回滚了。用户明明下单成功却收到下单失败的提示产生了大量客诉。从那以后我在项目里定了两个纪律主链路服务publishEvent后事件里只携带必要信息如订单ID监听器绝对不能反向依赖主服务查询最新状态并做影响主流程的操作。同步监听器必须在方法内兜住所有异常并打traceId日志。如果确实需要异步且对一致性要求高单独设计事件表中的处理状态和重试机制用定时任务扫描做补偿。7.4 适配器模式不等于无脑加壳用适配器模式封装外部SDK时最容易出现的误用是把转换数据格式和适配接口混做一团。适配器只解决接口签名不一致的问题如果你在适配器里做大量的业务逻辑处理比如折扣计算、状态流转、数据聚合那这个适配器就变成混沌层了。正确做法是适配器内部只做数据对象转换和方法映射业务逻辑放在被适配方调用前或调用后的Service层。我在代码审查时会明确要求适配器类里不允许出现业务规则判断如果出现了就要拆出来。7.5 装饰器和过滤器之间的关系要理清很多初学者会混淆装饰器模式和过滤器/拦截器。Spring里的Filter本身就是一种链式责任模式而HttpServletRequestWrapper是装饰器。两者的区别在于过滤器解决的是请求进入目标前的一串处理管道装饰器解决的是增强某个对象本身的能力。我在做网关鉴权时往往两者都用Filter负责身份校验判定是否放行装饰器负责包一层增加签名字段。混淆了会导致代码里职责不清出现过滤器里写业务逻辑业务代码里又去解析请求头这种混乱局面。7.6 最后一点小经验谈九个模式与Spring Boot的对应关系本质上是两件事一是读懂框架的捷径——框架已经帮你实现了大量模式你只需要在正确的位置扩展二是写好业务代码的地图——知道复杂的业务结构该怎么划分解耦、控制变化。对自己项目里反复出现的那几种代码坏味道去问一句哪个模式能救它往往答案就是最合适的设计。我偏爱的一句话是设计模式不是要你多写代码而是让你少写那些未来要推翻重写的代码。你一旦在Spring Boot项目里建立起这种角度看代码的习惯看开源项目的源码会顺很多自己做架构设计时心里也有底。这九种模式看着是九种知识的并列实际组合起来才是真正的工程能力希望这篇文章能帮你把它们串起来。
阅读完成 · 觉得有帮助?