Java Composition本质:对象职责建模与生命周期管理
1. Composition 不是“组合”,而是 Java 中被严重低估的建模基石
很多人第一次看到 “Composition in Java Example” 这个标题,下意识会想:“哦,就是讲怎么把几个类拼在一起用吧?不就是 new 一个对象再 set 进去吗?”——这种理解错得非常典型,也错得非常危险。它直接导致大量 Java 开发者在真实项目中写出大量难以维护、无法测试、边界模糊的代码。我带过三届校招新人,几乎每届都有人把 Composition 和 Inheritance 混为一谈,甚至在 Spring Boot 项目里用继承去“复用” Service 层逻辑,结果改一个方法,五个模块同时报空指针。
Composition 的本质,不是语法层面的“把 A 类放进 B 类里”,而是语义层面的“B 由 A 构成,A 是 B 的一部分,A 的生命周期依附于 B”。这个定义里藏着三个关键判断锚点:has-a 关系是否真实存在?A 是否可替换?A 的销毁是否由 B 控制?比如,“汽车 has-a 发动机”成立,但“汽车 is-a 发动机”显然荒谬;发动机可以被不同厂商的型号替换(松耦合),而一旦汽车报废,发动机通常也跟着进回收站(生命周期绑定)。这三点,才是 Composition 区别于 Association(关联)、Aggregation(聚合)甚至 Inheritance(继承)的分水岭。
你在网上搜到的绝大多数“Composition 示例”,只停留在class Car { private Engine engine; }这一行代码上,然后就跳到engine.start()。这根本不是 Composition,这只是“持有引用”。真正的 Composition 必须回答:这个 engine 是谁创建的?谁负责关闭它?如果 engine 初始化失败,car 能否进入可用状态?它的配置参数从哪来?这些细节,恰恰是面试官在问“请手写一个 Composition 示例”时真正想听的——他要的不是语法正确性,而是你对对象职责边界的认知深度。
这也是为什么“desktop composition is disabled”这类系统级错误提示会高频出现在 Java 开发者搜索记录里:表面看是 Windows 桌面特效开关问题,深层反映的是开发者对“composition”一词在不同上下文中的语义混淆。图形界面里的 desktop composition 指的是像素图层的合成渲染,而 Java 编程中的 composition 指的是对象职责的合成建模。两个词同源,但领域完全不同。混淆它们,就像把“数据库索引”和“图书目录索引”当成一回事——都叫索引,但一个靠 B+ 树,一个靠人工编目。
所以,本文不提供“教科书式”的 Composition 定义,而是带你从零开始,亲手构建一个真实场景下的 Composition 实现:一个支持热插拔协议解析器的网络消息处理器。它会强制你面对初始化顺序、资源释放、异常传播、依赖注入边界等所有 Composition 的核心痛点。你将看到,一个看似简单的private final ProtocolParser parser;声明背后,藏着至少七层设计决策。
2. 为什么不用继承?一次真实线上事故的复盘
去年我们一个支付网关服务突然出现偶发性超时,监控显示 99% 的请求耗时正常,但总有 0.3% 的请求卡在MessageProcessor.process()方法里长达 15 秒以上,最终触发熔断。排查三天后,定位到问题根源竟是一段三年前写的“优雅复用”代码:
// 当年为了快速支持新协议,写了这个继承结构 public class XmlMessageProcessor extends JsonMessageProcessor { @Override public Message process(String raw) { // 先调父类解析 JSON,再额外处理 XML 特有字段 Message msg = super.process(raw); parseXmlSpecificFields(msg, raw); return msg; } }问题出在哪?表面上看,XmlMessageProcessor确实“复用”了JsonMessageProcessor的解析逻辑。但JsonMessageProcessor内部持有一个JsonParser实例,该实例在构造时会加载 Jackson 的ObjectMapper并预热反射缓存。而XmlMessageProcessor的构造函数里,又悄悄 new 了一个SAXParser实例。这两个解析器共享同一个线程池和内存缓冲区,当高并发 XML 请求涌入时,SAXParser的 SAX 事件回调会抢占ObjectMapper的线程资源,导致 JSON 解析器内部锁等待,进而阻塞整个process()方法。
这就是典型的Inheritance 误用引发的隐式耦合。XmlMessageProcessor继承JsonMessageProcessor,意味着它必须完全理解并承担父类的所有实现细节、资源消耗模式和线程安全契约。而 Composition 的解法,是让两者彻底解耦:
public class MessageProcessor { private final JsonParser jsonParser; private final XmlParser xmlParser; // 构造时明确声明依赖,且类型抽象化 public MessageProcessor(JsonParser jsonParser, XmlParser xmlParser) { this.jsonParser = Objects.requireNonNull(jsonParser); this.xmlParser = Objects.requireNonNull(xmlParser); } public Message process(String raw) { if (isXml(raw)) { return xmlParser.parse(raw); // 完全独立的资源、线程、生命周期 } else { return jsonParser.parse(raw); } } }这里的关键转变有三点:
- 控制权反转:不再由
MessageProcessor自己决定创建哪个解析器,而是由外部(比如 Spring 容器或工厂类)按需注入。MessageProcessor只关心“能用”,不关心“怎么造”。 - 接口隔离:
JsonParser和XmlParser都实现统一的Parser<Message>接口,MessageProcessor对具体实现一无所知。新增ProtobufParser时,只需实现接口,无需修改MessageProcessor任何一行代码。 - 生命周期解耦:
JsonParser和XmlParser的初始化、销毁、配置都可以独立管理。JsonParser可以用单例模式全局复用,XmlParser可以按请求创建新实例避免状态污染。
那次事故后,我们团队立下铁规:任何新功能,禁止使用 extends 关键字实现业务逻辑复用。必须先画出对象关系图,确认是 is-a 还是 has-a;如果是 has-a,必须用 final 字段 + 构造注入,并在单元测试中 mock 所有依赖。这条规则看似死板,却让我们后续两年的线上故障率下降了 67%。因为 Composition 强制你把“谁负责什么”这件事,提前想清楚、写明白、测到位。
3. 从零构建一个生产级 Composition 示例:ProtocolRouter
现在,我们动手实现一个真正能上生产环境的 Composition 示例:ProtocolRouter。它不是一个玩具类,而是一个支撑日均 2 亿请求的消息路由核心。它的职责很明确:接收原始字节流,识别协议类型(HTTP/HTTPS/WebSocket/自定义二进制),将数据路由给对应的协议处理器,并确保处理器的资源被正确管理。这个例子将覆盖 Composition 的全部核心实践。
3.1 核心接口设计:用抽象隔离变化
Composition 的起点永远是接口。我们不从具体类开始,而是先定义协议处理器的行为契约:
/** * 协议处理器接口:所有具体处理器必须实现此接口 * 重点:定义了初始化、处理、销毁三个生命周期方法 */ public interface ProtocolHandler { /** * 初始化处理器,加载配置、建立连接池等 * @throws HandlerInitializationException 初始化失败时抛出 */ void initialize() throws HandlerInitializationException; /** * 处理单条消息,返回处理结果 * @param message 原始消息字节流 * @return 处理后的响应 */ Response handle(byte[] message); /** * 销毁处理器,释放所有资源(连接、缓存、线程池) */ void destroy(); }这个接口的设计暗含 Composition 的哲学:把“做什么”(handle)和“何时做”(initialize/destroy)清晰分离。initialize()和destroy()方法的存在,就是 Composition 对生命周期管理的硬性要求。没有这两个方法,你就无法保证ProtocolRouter在启动时正确初始化所有处理器,在关闭时安全释放所有资源。
接着,我们定义路由策略接口,它决定了“哪个消息该交给哪个处理器”:
/** * 路由策略:根据消息特征选择处理器 * Composition 的灵活性正体现在此处——策略可自由替换 */ public interface RoutingStrategy { /** * 根据消息头、长度、魔数等特征,返回匹配的处理器 * @param message 原始消息 * @return 匹配的处理器,null 表示无匹配 */ ProtocolHandler selectHandler(byte[] message); }注意,RoutingStrategy的selectHandler方法返回的是ProtocolHandler接口,而不是某个具体实现类。这意味着ProtocolRouter对具体策略一无所知,它可以是基于 HTTP Header 的HeaderBasedStrategy,也可以是基于 TLS 握手包的TlsHandshakeStrategy,甚至可以是机器学习模型驱动的MLRoutingStrategy。只要它们实现了接口,ProtocolRouter就能无缝集成。
3.2 ProtocolRouter 的 Composition 结构:final 字段与构造注入
ProtocolRouter类本身,就是 Composition 的典范。它不继承任何东西,只通过final字段持有其组成部分的引用,并在构造时强制注入:
public class ProtocolRouter { // 所有依赖都声明为 final,确保不可变性与线程安全基础 private final List<ProtocolHandler> handlers; private final RoutingStrategy routingStrategy; private final Logger logger; private final AtomicBoolean isRunning = new AtomicBoolean(false); /** * 构造函数:强制注入所有依赖,体现 Composition 的“组装”思想 * @param handlers 所有可用的协议处理器列表 * @param routingStrategy 路由策略 * @param logger 日志器(也是 Composition 的一部分!) */ public ProtocolRouter(List<ProtocolHandler> handlers, RoutingStrategy routingStrategy, Logger logger) { // 防御性检查:确保 Composition 的根基稳固 this.handlers = Objects.requireNonNull(handlers, "handlers must not be null"); this.routingStrategy = Objects.requireNonNull(routingStrategy, "routingStrategy must not be null"); this.logger = Objects.requireNonNull(logger, "logger must not be null"); // 验证:所有处理器必须已初始化,否则 Router 无法工作 for (ProtocolHandler handler : handlers) { if (!(handler instanceof InitializedHandler)) { throw new IllegalArgumentException( "All handlers must implement InitializedHandler or be pre-initialized"); } } } /** * 启动 Router:依次初始化所有处理器 * 这是 Composition 生命周期管理的关键一步 */ public void start() throws RouterStartException { if (!isRunning.compareAndSet(false, true)) { return; // 已启动,避免重复初始化 } logger.info("Starting ProtocolRouter with {} handlers", handlers.size()); for (int i = 0; i < handlers.size(); i++) { try { handlers.get(i).initialize(); logger.debug("Handler {} initialized successfully", i); } catch (HandlerInitializationException e) { // 关键设计:单个处理器初始化失败,应停止整个 Router 启动 // 这体现了 Composition 的整体性:部件失效,系统不可用 logger.error("Failed to initialize handler {}", i, e); stop(); // 立即清理已初始化的部分 throw new RouterStartException("Failed to start router: " + e.getMessage(), e); } } logger.info("ProtocolRouter started successfully"); } /** * 处理消息:Composition 的核心行为 * @param message 原始字节流 * @return 处理结果 */ public Response route(byte[] message) { if (!isRunning.get()) { throw new IllegalStateException("Router is not running. Call start() first."); } ProtocolHandler handler = routingStrategy.selectHandler(message); if (handler == null) { logger.warn("No handler found for message of length {}", message.length); return Response.error("Unsupported protocol"); } try { return handler.handle(message); } catch (Exception e) { logger.error("Error handling message with handler {}", handler.getClass().getSimpleName(), e); return Response.error("Internal processing error"); } } /** * 停止 Router:依次销毁所有处理器,确保资源释放 * Composition 的闭环管理 */ public void stop() { if (!isRunning.compareAndSet(true, false)) { return; // 已停止 } logger.info("Stopping ProtocolRouter..."); // 倒序销毁:通常后初始化的先销毁,避免依赖冲突 for (int i = handlers.size() - 1; i >= 0; i--) { try { handlers.get(i).destroy(); logger.debug("Handler {} destroyed", i); } catch (Exception e) { logger.error("Error destroying handler {}", i, e); // 销毁失败不中断整体流程,继续销毁其他处理器 } } logger.info("ProtocolRouter stopped"); } }这段代码里,ProtocolRouter的 Composition 结构一目了然:
handlers:一组ProtocolHandler,代表其“能力集合”。它们是 Router 的“器官”。routingStrategy:决定如何调度这些“器官”的“大脑”。logger:负责记录所有关键事件的“神经系统”。
所有字段都是final,确保一旦组装完成,其组成结构就不可更改。构造函数的参数列表,就是这个对象的“装配说明书”。任何试图绕过构造函数直接 new 一个ProtocolRouter的做法,都会因缺少必要依赖而编译失败——这是编译器强制你遵守 Composition 规则的第一道防线。
提示:
AtomicBoolean isRunning的存在,是 Composition 在并发场景下的必然要求。它不是一个“功能”,而是一个状态协调器,用于同步start()和stop()的执行,确保route()方法不会在 Router 未启动或已停止的状态下被调用。这个小细节,正是 Composition 思维深入骨髓的体现:每一个状态,都必须有明确的归属和管理责任。
3.3 具体处理器实现:HttpHandler 的完整生命周期
现在,我们来实现一个具体的ProtocolHandler:HttpHandler。它将展示 Composition 如何将一个复杂的功能(HTTP 协议解析)分解为更小的、可独立管理的部件。
public class HttpHandler implements ProtocolHandler { // HttpHandler 的 Composition 结构:它自己也是一个“组装体” private final HttpRequestParser requestParser; // 解析 HTTP 请求行和头 private final HttpResponseBuilder responseBuilder; // 构建 HTTP 响应 private final ConnectionPool connectionPool; // 管理底层 TCP 连接 private final RateLimiter rateLimiter; // 限流器,防止过载 // 所有部件都通过构造注入,且均为 final public HttpHandler(HttpRequestParser requestParser, HttpResponseBuilder responseBuilder, ConnectionPool connectionPool, RateLimiter rateLimiter) { this.requestParser = Objects.requireNonNull(requestParser); this.responseBuilder = Objects.requireNonNull(responseBuilder); this.connectionPool = Objects.requireNonNull(connectionPool); this.rateLimiter = Objects.requireNonNull(rateLimiter); } @Override public void initialize() throws HandlerInitializationException { try { // 初始化所有子部件 requestParser.initialize(); responseBuilder.initialize(); connectionPool.initialize(); rateLimiter.initialize(); // 额外的初始化逻辑:预热连接池 connectionPool.preheat(10); // 预热 10 个连接 } catch (Exception e) { throw new HandlerInitializationException("Failed to initialize HttpHandler", e); } } @Override public Response handle(byte[] message) { // 1. 限流检查 if (!rateLimiter.tryAcquire()) { return Response.error("Too many requests"); } // 2. 解析请求 HttpRequest request; try { request = requestParser.parse(message); } catch (ParseException e) { return Response.error("Invalid HTTP request: " + e.getMessage()); } // 3. 从连接池获取连接 Connection conn; try { conn = connectionPool.acquire(); } catch (ConnectionPoolException e) { return Response.error("Service unavailable"); } // 4. 处理业务逻辑(此处简化为模拟) String responseBody = processBusinessLogic(request); // 5. 构建响应 byte[] responseBytes = responseBuilder.build( 200, "OK", Map.of("Content-Type", "text/plain"), responseBody.getBytes(StandardCharsets.UTF_8) ); // 6. 归还连接 connectionPool.release(conn); return Response.success(responseBytes); } @Override public void destroy() { // 销毁所有子部件,顺序与初始化相反 connectionPool.destroy(); rateLimiter.destroy(); responseBuilder.destroy(); requestParser.destroy(); } private String processBusinessLogic(HttpRequest request) { // 真实业务逻辑,此处省略 return "Hello from HttpHandler"; } }HttpHandler本身就是一个 Composition 的嵌套实例。它不自己实现 HTTP 解析、连接管理、限流等任何功能,而是将这些职责委托给更小、更专注的部件(HttpRequestParser,ConnectionPool等)。每个部件都拥有自己的initialize()和destroy()方法,HttpHandler的initialize()和destroy()方法,就是对这些子部件生命周期的编排。
这种层层嵌套的 Composition,构建出了一个健壮、可测试、易扩展的系统。你可以单独为HttpRequestParser写单元测试,mock 掉ConnectionPool;你可以为ConnectionPool写压力测试,验证其在 10000 并发下的表现;你甚至可以将HttpHandler的ConnectionPool替换为一个内存版的MockConnectionPool,用于快速集成测试。这一切,都源于 Composition 对“关注点分离”和“生命周期显式化”的坚持。
4. Composition 的陷阱与避坑指南:那些文档里不会写的实战经验
Composition 理念虽好,但在真实项目落地时,会遇到一堆文档里绝不会提及的“灰色地带”问题。这些问题往往不会导致编译失败,却会让代码在生产环境里悄无声息地腐烂。以下是我在多个大型 Java 项目中踩过的坑,以及总结出的硬核避坑指南。
4.1 陷阱一:循环依赖——不是架构问题,是设计信号
最经典的循环依赖场景是UserService和EmailService:
// UserService.java public class UserService { private final EmailService emailService; public UserService(EmailService emailService) { this.emailService = emailService; } public void createUser(User user) { // ... 创建用户逻辑 emailService.sendWelcomeEmail(user); // 依赖 EmailService } } // EmailService.java public class EmailService { private final UserService userService; public EmailService(UserService userService) { this.userService = userService; } public void sendWelcomeEmail(User user) { // ... 发送邮件逻辑 userService.updateUserStatus(user.getId(), "EMAIL_SENT"); // 反向依赖 UserService } }Spring 容器可能会用三级缓存勉强解决这个循环依赖,但这是饮鸩止渴。循环依赖从来不是 Spring 的 bug,而是你领域模型设计错误的强烈信号。它表明UserService和EmailService的职责边界已经模糊不清,它们本应是两个独立的、松耦合的组件,却被强行绑在了一起。
避坑方案:引入事件总线(Event Bus)
将“发送欢迎邮件”这个动作,从一个同步的、强耦合的方法调用,改为一个异步的、松耦合的事件发布:
// 定义事件 public class UserCreatedEvent { private final User user; public UserCreatedEvent(User user) { this.user = user; } // getter... } // UserService 不再依赖 EmailService public class UserService { private final EventPublisher eventPublisher; // 事件发布器,一个轻量级依赖 public UserService(EventPublisher eventPublisher) { this.eventPublisher = eventPublisher; } public void createUser(User user) { // ... 创建用户逻辑 eventPublisher.publish(new UserCreatedEvent(user)); // 发布事件,不关心谁消费 } } // EmailService 订阅事件 public class EmailService implements EventHandler<UserCreatedEvent> { @Override public void handle(UserCreatedEvent event) { // ... 发送邮件逻辑 // 此处可以安全地调用 UserService 的查询方法,但绝不能调用更新方法 // 因为事件处理是异步的,且发生在用户创建之后 } }这个改动,将UserService和EmailService之间的强依赖(UserService -> EmailService -> UserService)打破,变成了UserService -> EventPublisher和EmailService <- EventPublisher的两个单向依赖。EventPublisher是一个极简的、无状态的基础设施组件,它不包含任何业务逻辑,因此不会引发新的循环依赖。这才是 Composition 应有的样子:部件之间通过清晰、单向的契约进行协作。
4.2 陷阱二:过度设计的“可插拔”——让简单问题复杂化
很多开发者受“设计模式八股文”影响,一上来就想搞“高度可插拔”。于是,一个本可以用if-else判断的协议识别逻辑,被硬生生拆成ProtocolDetector接口、HttpDetector、WebSocketDetector、CustomBinaryDetector三个实现类,再加一个DetectorFactory和DetectorRegistry。代码量翻了五倍,可读性暴跌,而实际业务需求可能未来五年都不会增加第四种协议。
避坑方案:YAGNI(You Aren't Gonna Need It)原则 + 渐进式重构
我的经验是:先用最简单、最直接的方式实现第一个需求,然后在第二个需求出现时,再提取公共部分。比如,最初只有 HTTP:
public class SimpleProtocolRouter { public Response route(byte[] message) { if (isHttp(message)) { return httpHandler.handle(message); } throw new UnsupportedOperationException("Unknown protocol"); } }当 WebSocket 需求来了,先写一个isWebSocket(message)判断,加到if-else里。当第三个协议来了,if-else变得臃肿时,再考虑提取ProtocolDetector接口。此时,你已经有了三个真实的、经过验证的实现类,提取接口的风险极低,且能精准抓住共性。
注意:这里的“简单”不是指代码少,而是指心智负担小、修改成本低。一个 20 行的
if-else,远比一个需要理解 5 个新类、3 个新接口、1 个工厂模式的“优雅”设计更容易维护。Composition 的终极目标是降低复杂度,而不是制造新的复杂度。
4.3 陷阱三:资源泄漏——finalize() 是个陷阱,不是救星
很多开发者认为,只要在destroy()方法里释放了资源,就万事大吉。但他们忽略了 JVM 的 GC 机制:destroy()是手动调用的,而finalize()方法是 JVM 在对象被 GC 前自动调用的,且JVM 不保证finalize()一定会被调用,也不保证调用时机。这意味着,如果你只依赖finalize()来释放文件句柄、数据库连接等稀缺资源,你的应用迟早会因“Too many open files”而崩溃。
避坑方案:使用 try-with-resources + 显式 destroy()
Java 7 引入的try-with-resources语句,是 Composition 资源管理的黄金搭档。它要求资源类实现AutoCloseable接口,而AutoCloseable.close()方法,就是destroy()的标准命名:
// 让 ProtocolHandler 实现 AutoCloseable public interface ProtocolHandler extends AutoCloseable { void initialize() throws HandlerInitializationException; Response handle(byte[] message); @Override void close(); // 标准的 destroy() 方法名 } // 在 Router 的 stop() 方法中,使用 try-with-resources 的语义 public void stop() { if (!isRunning.compareAndSet(true, false)) return; // 使用 Collections.synchronizedList 或 CopyOnWriteArrayList 确保线程安全 for (ProtocolHandler handler : new ArrayList<>(handlers)) { try { handler.close(); // 显式调用 } catch (Exception e) { logger.error("Error closing handler", e); } } }更重要的是,在业务代码中,也要养成try-with-resources的习惯:
public Response processWithSafety(byte[] message) { // 创建一个临时的、轻量级的处理器实例 try (ProtocolHandler tempHandler = new TempHttpHandler()) { tempHandler.initialize(); return tempHandler.handle(message); } catch (Exception e) { logger.error("Error in temporary handler", e); return Response.error("Processing failed"); } }try-with-resources保证了无论handle()方法是正常返回还是抛出异常,tempHandler.close()都会被执行。这比任何finally块都更可靠,也比寄希望于finalize()更务实。Composition 的资源管理,必须是确定性的、可预测的、可测试的。
4.4 陷阱四:测试困境——如何 Mock 一个“组合体”
当你想为ProtocolRouter写单元测试时,会发现它依赖List<ProtocolHandler>和RoutingStrategy。如果直接 new 一个真实的HttpHandler,测试就会变得又慢又脆弱(因为它会尝试连接真实的网络、数据库)。你需要 Mock 它们。
避坑方案:使用 Mockito 的@Mock和@InjectMocks,但要理解其原理
@RunWith(MockitoJUnitRunner.class) public class ProtocolRouterTest { @Mock private ProtocolHandler httpHandler; @Mock private ProtocolHandler wsHandler; @Mock private RoutingStrategy routingStrategy; @Mock private Logger logger; @InjectMocks private ProtocolRouter router; // Mockito 会自动将上面的 @Mock 注入到这里 @Before public void setUp() { // 初始化 Router,传入 Mock 对象列表 List<ProtocolHandler> handlers = Arrays.asList(httpHandler, wsHandler); this.router = new ProtocolRouter(handlers, routingStrategy, logger); } @Test public void testRouteToHttpHandler() { byte[] httpMessage = "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n".getBytes(); // 配置 Mock:当 routingStrategy.selectHandler 被调用时,返回 httpHandler when(routingStrategy.selectHandler(httpMessage)).thenReturn(httpHandler); when(httpHandler.handle(httpMessage)).thenReturn(Response.success(new byte[0])); // 执行测试 Response response = router.route(httpMessage); // 验证结果和交互 assertThat(response.isSuccess()).isTrue(); verify(httpHandler).handle(httpMessage); // 验证 httpHandler.handle 被调用 verifyNoMoreInteractions(wsHandler); // 验证 wsHandler 没有被调用 } }关键点在于@InjectMocks。它不是魔法,而是 Mockito 在运行时,通过反射找到router对象中所有private final字段(如handlers,routingStrategy,logger),然后将@Mock标记的对象,按照类型匹配,注入进去。这完美契合了 Composition 的构造注入模式。如果你的ProtocolRouter是用setter方法注入依赖的,@InjectMocks就无法工作,你必须手动调用setXXX()方法。所以,坚持使用构造注入,不仅是设计原则,更是为了测试便利性。
5. Composition 与 Java 生态:Spring、Lombok 和现代开发实践
Composition 不是孤立存在的理念,它深深植根于 Java 的现代开发生态中。理解它与主流框架、工具的协同关系,才能真正将其融入日常开发。
5.1 Spring Framework:Composition 的最佳实践平台
Spring 的核心思想,就是 Composition。@Component,@Service,@Repository这些注解,本质上就是在告诉 Spring 容器:“请帮我创建这个对象,并把它作为 Composition 的一个部件。” 而@Autowired,就是 Spring 为你自动完成的构造注入。
@Service public class OrderService { private final PaymentService paymentService; // final 字段 private final InventoryService inventoryService; // 构造注入,Spring 5+ 推荐方式 public OrderService(PaymentService paymentService, InventoryService inventoryService) { this.paymentService = paymentService; this.inventoryService = inventoryService; } public Order createOrder(OrderRequest request) { // ... 业务逻辑 paymentService.charge(request.getPaymentInfo()); // 依赖注入的部件 inventoryService.reserve(request.getItems()); // 依赖注入的部件 return new Order(); } }Spring 容器在启动时,会扫描所有@Component类,根据它们的构造函数参数,递归地创建并注入所有依赖。这整个过程,就是 Composition 的自动化装配。你不需要手动 new 一堆对象再塞进去,Spring 会帮你搞定。这极大地降低了 Composition 的使用门槛,但也带来一个风险:过度依赖 Spring 的自动装配,会让你忘记 Composition 的本质。你应该始终能清晰地说出:OrderService由哪些部件构成?每个部件的职责是什么?它们的生命周期如何管理?如果某天你离开 Spring,换用 Micronaut 或 Quarkus,这些知识依然有效。
5.2 Lombok:减少样板代码,聚焦 Composition 本质
Lombok 的@RequiredArgsConstructor和@AllArgsConstructor注解,是 Composition 的强力加速器。它们自动生成构造函数,让你可以专注于定义final字段,而不必为每个字段手写this.field = field。
@Service @RequiredArgsConstructor // 自动生成只包含 final 字段的构造函数 public class UserService { private final UserRepository userRepository; // final,必须注入 private final EmailService emailService; // final,必须注入 private final Logger logger; // final,必须注入 // Lombok 自动生成: // public UserService(UserRepository userRepository, // EmailService emailService, // Logger logger) { // this.userRepository = userRepository; // this.emailService = emailService; // this.logger = logger; // } public User createUser(User user) { User saved = userRepository.save(user); emailService.sendWelcomeEmail(saved); return saved; } }@RequiredArgsConstructor只为final和@NonNull字段生成参数,这完美契合了 Composition 的要求:所有必需的部件,都必须在构造时提供。它用一行注解,就强制团队遵守了 Composition 的最佳实践。当然,Lombok 也有陷阱,比如@Data会生成toString()、equals()等方法,如果类里有敏感字段(如密码),就必须手动重写toString()来排除。但这属于另一个话题了。
5.3 Java 14+ Record:为不可变数据建模的 Composition 新范式
Java 14 引入的Record,是 Composition 在数据建模领域的革命。它天生就是不可变的、透明的、基于值的。一个Record,天然就是一个 Composition 的“数据部件”。
// 一个纯粹的数据载体,没有行为,只有结构 public record HttpRequest( HttpMethod method, String path, Map<String, String> headers, byte[] body ) { // 可以添加静态工厂方法,增强可读性 public static HttpRequest ofGet(String path) { return new HttpRequest(HttpMethod.GET, path, Map.of(), new byte[0]); } } // 在 ProtocolHandler 中,它就是一个完美的输入/输出部件 public interface ProtocolHandler { // 输入是 HttpRequest Record,输出是 HttpResponse Record HttpResponse handle(HttpRequest request); }Record的不可变性,与 Composition 的final字段精神高度一致。它消除了数据在传递过程中被意外修改的风险,让ProtocolRouter的各个部件之间,可以放心地共享数据,而不用担心状态污染。这是 Composition 在数据流层面的自然延伸。
6. Composition 的终极价值:不是代码技巧,而是思维范式
写完这个ProtocolRouter示例,你可能会觉得:“哦,原来 Composition 就是多写几个接口,多用几个final字段,再把new换成@Autowired。” 如果你只看到这一层,那这篇文章就失败了。Composition 的终极价值,远不止于此。
它是一种对抗软件熵增的思维范式。软件系统天生趋向混乱:需求变更、人员流动、技术迭代,都会像热力学第二定律一样,让代码库的“熵”不断增加。而 Composition,就是一套行之有效的“负熵”机制。它通过强制你回答以下问题,来持续对抗混乱:
- 这个类的职责到底是什么?(回答不了,就说明它违反了单一职责原则,应该被拆分)
- 它由哪些更小的、更专注的部件构成?(回答不了,就说明它还不够“组合”,还太“原子”)
- 这些部件的生命周期,是由谁来管理的?(回答不了,就说明资源管理是模糊的,迟早泄漏)
- 如果我要替换掉其中某一个部件,需要修改多少其他代码?(修改越多,耦合越紧,Composition 越失败)
每一次你认真思考并回答这些问题,你都在为系统的长期健康投票。你写的不是一段代码,而是一份关于“这个系统应该如何被理解和演进”的契约。这份契约,比任何框架、任何语法糖都重要。
所以,下次当你看到一个“Java Composition Example”的搜索请求,不要急着去复制粘贴一个class A { private B b; }的例子。停下来,问问自己:在这个例子的场景里,A和B之间,是否存在真实的“has-a”关系?B的创建、配置、销毁,是否都由A明确掌控?如果B初始化失败,A是否还能作为一个可用的整体存在?如果答案是否定的,那么你看到的,很可能只是一个披着 Composition 外衣的、脆弱的、不可维护的代码片段。
Composition 不是终点,而是一个起点。它始于一个final字段的声明,成于无数次对“职责”与“边界”的审慎思考,终于一个在风雨中依然稳健运行的系统。这条路没有捷径,但每一步,都算数。
