软件架构中模块实例化设计:从依赖注入到生命周期管理
1. 项目概述:从“模块”到“实例”的鸿沟
在软件架构设计,尤其是中大型系统的开发中,“模块化”是一个被反复提及和推崇的核心理念。我们常常听到这样的架构描述:“系统分为用户模块、订单模块、支付模块、商品模块……”,听起来清晰明了。然而,当这些静态的、概念上的“模块”图纸,需要落地为一个个在内存中运行、承载具体业务数据、处理实时请求的“活”对象时,一道巨大的鸿沟便出现了。这道鸿沟,就是“实例化设计”。
“系统模块与子模块的实例化设计”这个主题,直指的就是如何跨越这道鸿沟。它探讨的不是“模块应该怎么划分”(那是领域划分或架构设计),而是“划分好的模块,在代码运行时,应该如何被创建、组装、管理和销毁”。这关乎系统的启动性能、内存占用、资源管理、依赖解耦,乃至整个应用的生命周期管理。一个糟糕的实例化设计,会让最精妙的模块划分变得举步维艰,系统可能启动缓慢、内存泄漏、模块间形成死锁依赖,或者根本无法进行有效的单元测试。
我经历过不少项目,初期架构图美轮美奂,模块边界清晰,但一到编码实现,就变成了在main函数或某个启动类里进行长达数百行的“new A(), new B(), setXXX()”硬编码组装。随着模块增多,这个启动代码变成了无人敢动的“祖传代码”,任何模块的增减或依赖变更都意味着要在这个庞然大物里小心翼翼地修改,风险极高。这正是缺乏系统化实例化设计所带来的典型困境。
本文将从一个资深开发者的视角,深入拆解模块实例化设计的核心模式、技术选型考量、具体实现细节以及那些只有踩过坑才知道的实践经验。无论你是在设计一个全新的微服务架构,还是在重构一个历史遗留的单体应用,希望这些内容能为你提供可直接参考的“施工蓝图”。
2. 核心设计思路:从“硬编码”到“声明式”的演进
模块的实例化,本质上是一个对象的创建与依赖注入过程。其设计思路的演进,清晰地反映了软件工程从“作坊式”到“工程化”的发展路径。理解这种演进,有助于我们做出更合适的技术选型。
2.1 原始阶段:硬编码与过程式组装
这是最直接,也是最脆弱的方式。所有模块的创建和依赖关系,都在一个或多个集中的、过程式的代码块中完成。
// 一个典型的“硬编码”启动类 public class ApplicationStarter { public static void main(String[] args) { // 1. 创建底层基础设施模块 ConfigManager config = new ConfigManager("app.properties"); DataSource dataSource = new MysqlDataSource(config); Logger logger = new FileLogger("/var/log/app.log"); // 2. 创建业务模块,并手动注入依赖 UserRepository userRepo = new UserRepositoryImpl(dataSource); OrderRepository orderRepo = new OrderRepositoryImpl(dataSource); PaymentService paymentService = new PaymentServiceImpl(config, logger); // 3. 创建上层服务模块,依赖更复杂 UserService userService = new UserServiceImpl(userRepo, logger); OrderService orderService = new OrderServiceImpl(orderRepo, userService, paymentService); // 4. 创建API/控制器层 UserController userController = new UserController(userService); OrderController orderController = new OrderController(orderService); // 5. 启动Web服务器,注册控制器... WebServer server = new WebServer(); server.register(userController); server.register(orderController); server.start(); } }为什么说这种方式有问题?
- 紧耦合:
ApplicationStarter对系统中每一个具体实现类都了如指掌,任何实现类的替换(比如FileLogger换成KafkaLogger)都需要修改此处。 - 难以测试:无法轻松地为
OrderService注入一个模拟的PaymentService进行单元测试,因为它的依赖是在启动时硬编码创建的。 - 代码膨胀:随着模块数量呈指数增长,这个启动函数会变得极其冗长和复杂。
- 生命周期管理缺失:谁负责关闭
DataSource?如何确保Logger在所有模块完成后才关闭?这些都需要额外的、容易出错的代码来处理。
2.2 进阶阶段:工厂模式与服务定位器
为了解耦对象的创建和使用,我们引入了工厂模式。同时,为了全局访问这些创建好的对象,服务定位器模式一度流行。
// 模块工厂 public class ModuleFactory { private static DataSource dataSource; private static Logger logger; public static UserService createUserService() { if (userRepo == null) { userRepo = new UserRepositoryImpl(getDataSource()); } return new UserServiceImpl(userRepo, getLogger()); } public static DataSource getDataSource() { if (dataSource == null) { dataSource = new MysqlDataSource(ConfigManager.getInstance()); } return dataSource; } // ... 其他工厂方法 } // 服务定位器 public class ServiceLocator { private static Map<String, Object> services = new HashMap<>(); public static void register(String name, Object service) { services.put(name, service); } public static <T> T get(String name, Class<T> type) { return type.cast(services.get(name)); } } // 使用方式 UserService userService = ModuleFactory.createUserService(); ServiceLocator.register("userService", userService); // 在另一个遥远的类中 UserService service = ServiceLocator.get("userService", UserService.class);为什么这依然不够好?
- 依赖隐藏:
UserServiceImpl的依赖(UserRepository,Logger)被隐藏在了工厂方法内部,破坏了类的接口契约,使得阅读代码时难以理清依赖关系。 - 全局状态:服务定位器本质上是一个全局注册表,它让模块的依赖变得隐式,不利于测试(需要先设置定位器),也容易导致线程安全问题。
- 类型不安全:服务定位器通常基于字符串或弱类型,容易在运行时出错。
2.3 现代阶段:依赖注入与控制反转容器
这是当前的主流和最佳实践。其核心思想是控制反转:对象的依赖不再由对象自己创建或查找,而是由外部容器在创建对象时主动注入。框架(如Spring, Guice)扮演了这个容器的角色。
这种方式从“硬编码”的命令式编程,转向了声明式编程。我们不再写“如何创建”的指令,而是声明“我需要什么”以及“我是什么”。
// 声明依赖(通过构造函数,这是最推荐的方式) @Service // 声明这是一个需要被容器管理的服务模块 public class UserServiceImpl implements UserService { private final UserRepository userRepo; private final Logger logger; @Autowired // 声明需要容器注入此依赖 public UserServiceImpl(UserRepository userRepo, Logger logger) { this.userRepo = userRepo; this.logger = logger; } // ... 业务方法 } // 配置类(Java Config方式,替代XML) @Configuration public class AppConfig { @Bean // 声明这是一个由容器创建的Bean(模块实例) public DataSource dataSource() { return new HikariDataSource(); // 这里可以复杂地配置连接池 } @Bean public Logger logger() { return new LogbackLogger(); } }为什么依赖注入容器是更优解?
- 彻底解耦:模块类只关心自己的业务逻辑和需要的依赖接口,完全不关心依赖的具体实现和创建过程。
- 易于测试:可以轻松地通过构造器注入模拟对象进行单元测试。
- 集中配置与管理:容器的配置中心化,所有对象的生命周期(单例、原型)、作用域、依赖关系一目了然。
- 强大的扩展能力:容器通常提供AOP、事件监听、条件化加载等高级特性,这些都能无缝应用到所有由容器管理的模块上。
注意:选择依赖注入框架时,Spring Framework 是Java生态的事实标准,功能全面但较重;Google Guice 更轻量、更纯粹;对于小型或特定项目,手动实现一个简单的依赖注入容器也是可行的,但这需要精心设计。
3. 实例化模式详解:单例、原型与作用域
在容器中,模块并非总是被创建一次。不同的业务场景需要不同的实例化模式。理解并正确运用这些模式,是实例化设计的关键。
3.1 单例模式:共享与状态管理
这是最常用的模式。容器中只创建该模块的一个实例,所有需要该依赖的地方都注入这同一个实例。
适用场景:
- 无状态服务:如各种
Service、Repository、工具类(StringUtils)。它们不持有与请求相关的状态,可以安全共享。 - 重量级资源:数据库连接池(
DataSource)、缓存客户端(RedisTemplate)、线程池。这些资源创建成本高,需要复用。 - 配置信息:全局的配置类。
实现与考量: 在Spring中,默认就是单例(@Scope(“singleton”))。你需要确保单例Bean是线程安全的。
@Service // 默认单例 public class StatisticsService { private final AtomicLong requestCount = new AtomicLong(0); // 使用线程安全的容器 public void recordRequest() { requestCount.incrementAndGet(); } // 即使有状态,也通过线程安全方式管理 }常见陷阱:
- 意外状态:在单例Bean中无意间使用了非线程安全的成员变量(如
SimpleDateFormat),会导致并发问题。解决方案是使用ThreadLocal或每次调用时创建新实例。 - 循环依赖:A依赖B,B也依赖A。单例模式下,容器在初始化时会陷入死锁。应通过设计(提取公共接口、使用setter注入、
@Lazy注解)避免循环依赖。
3.2 原型模式:每次都是新的
每次从容器中获取该模块时,都会创建一个新的实例。
适用场景:
- 有状态对象:每个请求或会话需要自己独立状态的对象。例如,一个处理特定订单流程的
OrderProcessor,其内部需要维护订单的处理状态。 - 非线程安全对象:如之前提到的
SimpleDateFormat,如果必须作为Bean,应声明为原型。 - 需要频繁改变配置的对象。
实现: 在Spring中使用@Scope(“prototype”)。
@Component @Scope("prototype") public class ReportGenerator { private ReportConfig config; public void setConfig(ReportConfig config) { this.config = config; // 每个ReportGenerator实例可以有不同的配置 } // ... 生成报告 }实操心得:原型Bean的依赖管理需要小心。如果单例Bean A依赖原型Bean B,那么A中注入的B实例在A的生命周期内是固定的(即只在A初始化时注入一次),并不会每次调用A的方法都获得新的B。如果需要每次都获取新的原型Bean,需要结合方法注入(
@Lookup)或ObjectFactory/Provider接口。
3.3 自定义作用域:连接生命周期与上下文
单例和原型有时不足以满足需求。例如在Web应用中,我们需要一个“请求作用域”的Bean,在一次HTTP请求内是单例,不同请求间则不同。或者“会话作用域”,绑定到用户会话。
Spring内置作用域:
request:一次HTTP请求。session:一个用户HTTP会话。application:一个ServletContext生命周期。websocket:一个WebSocket会话。
自定义作用域: 对于更复杂的场景,如一个后台任务处理管道,希望每个“任务”拥有自己的一组Bean实例,就可以自定义一个“task”作用域。
// 1. 实现Scope接口 public class TaskScope implements Scope { private final Map<String, Object> scopedObjects = new ConcurrentHashMap<>(); private final Map<String, Runnable> destructionCallbacks = new ConcurrentHashMap<>(); @Override public Object get(String name, ObjectFactory<?> objectFactory) { // 以任务ID作为key的一部分存储对象 String taskId = CurrentTaskContext.getId(); String key = taskId + ":" + name; return scopedObjects.computeIfAbsent(key, k -> objectFactory.getObject()); } @Override public void registerDestructionCallback(String name, Runnable callback) { destructionCallbacks.put(name, callback); } // 当任务完成时,调用此方法清理该任务作用域下的所有Bean public void endTask(String taskId) { String prefix = taskId + ":"; scopedObjects.keySet().removeIf(key -> key.startsWith(prefix)); // 执行销毁回调... } // ... 其他方法 } // 2. 注册自定义作用域到容器 @Configuration public class ScopeConfig implements BeanFactoryPostProcessor { @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) { beanFactory.registerScope("task", new TaskScope()); } } // 3. 使用自定义作用域 @Component @Scope("task") public class TaskSpecificProcessor { // 这个Bean在每个task作用域内是单例 }为什么需要自定义作用域?它完美地将模块实例的生命周期与你的业务上下文(如任务、事务、批处理作业)绑定在一起,实现了资源的精细化管理,避免了内存泄漏,也使得代码逻辑更清晰。
4. 模块依赖解析与循环依赖处理
依赖注入的核心是解析依赖关系图。一个复杂的系统,模块间的依赖关系可能形成一张复杂的网,甚至出现环,即循环依赖。
4.1 依赖解析策略:构造器 vs Setter vs 字段注入
容器注入依赖主要有三种方式,它们对实例化顺序、可测试性和设计清晰度有不同影响。
1. 构造器注入(强烈推荐)
@Service public class OrderService { private final OrderRepository orderRepo; private final PaymentService paymentService; @Autowired // Spring 4.3+ 在单个构造器时可省略 public OrderService(OrderRepository orderRepo, PaymentService paymentService) { this.orderRepo = orderRepo; this.paymentService = paymentService; } }- 优点:
- 不可变:依赖被声明为
final,确保Bean在实例化后依赖不可变,线程安全。 - 完全初始化:对象在构造完成后就处于完全可用状态。
- 清晰明确:类的依赖关系在构造器签名中一目了然。
- 利于测试:无需容器即可轻松通过new进行单元测试。
- 不可变:依赖被声明为
- 缺点:当依赖很多时,构造器参数列表会很长。
2. Setter方法注入
@Component public class OrderService { private OrderRepository orderRepo; private PaymentService paymentService; @Autowired public void setOrderRepository(OrderRepository orderRepo) { this.orderRepo = orderRepo; } // ... 其他setter }- 优点:允许对象在创建后再设置依赖,更灵活,可以解决某些循环依赖。
- 缺点:对象可能在依赖设置前被使用,状态不稳定。依赖关系不如构造器清晰。
3. 字段注入(不推荐用于主要业务Bean)
@Component public class OrderService { @Autowired private OrderRepository orderRepo; @Autowired private PaymentService paymentService; }- 优点:代码简洁。
- 致命缺点:
- 隐藏了依赖:无法从类外部一眼看出其依赖,破坏了封装性。
- 不利于测试:必须通过反射或容器来注入依赖,无法直接通过new进行测试。
- 可能导致NPE:因为依赖可能为null。
- 使字段无法声明为final。
个人实践:我团队中强制要求主要业务逻辑Bean必须使用构造器注入。只有在配置类或某些第三方库适配Bean中,才会酌情使用Setter或字段注入。这大大提升了代码的可维护性和可测试性。
4.2 循环依赖的识别、避免与解决
循环依赖是模块化设计中的一个“坏味道”,通常意味着职责划分不清。但在大型复杂系统中,有时难以完全避免。
场景:UserService需要RoleService来检查用户权限,而RoleService又需要UserService来获取角色所属的用户列表。
Spring的解决机制(三级缓存): Spring通过“提前暴露”正在创建中的Bean引用来解决单例Bean的Setter/字段注入循环依赖。但其解决能力有限:
- 只支持单例作用域的Bean。
- 如果循环依赖是构造器注入,Spring无法解决,会直接抛出
BeanCurrentlyInCreationException。
如何避免和解决?
最佳方案:重构设计
- 提取公共逻辑:将
UserService和RoleService都依赖的逻辑提取到一个新的AuthorizationService中。 - 使用接口与回调:通过事件或回调机制解耦,让其中一个服务在需要时通过接口调用另一个,而非直接持有引用。
- 合并服务:如果两个服务关系如此紧密,或许它们本应属于同一个聚合根,考虑合并。
- 提取公共逻辑:将
技术方案(如果重构成本过高)
- 改用Setter/字段注入:这是Spring能自动处理的情况。
- 使用
@Lazy注解:在其中一个依赖上添加@Lazy,告诉容器延迟初始化该Bean,打破初始化时的循环。
@Service public class UserService { private final RoleService roleService; public UserService(@Lazy RoleService roleService) { // 构造器注入也能用Lazy打破循环 this.roleService = roleService; } }- 使用
ObjectFactory或Provider:不直接注入Bean实例,而是注入一个能获取实例的工厂。
@Service public class UserService { private final ObjectFactory<RoleService> roleServiceFactory; public UserService(ObjectFactory<RoleService> roleServiceFactory) { this.roleServiceFactory = roleServiceFactory; } public void someMethod() { RoleService roleService = roleServiceFactory.getObject(); // 在需要时才获取 // ... } }
排查技巧:当启动报循环依赖错误时,Spring的异常信息通常会给出循环链(如A -> B -> C -> A)。根据这个链条,去检查这些Bean的注入方式,并应用上述方法进行解耦。
5. 高级实例化策略:条件化、动态与懒加载
在复杂的生产环境中,我们常常需要根据不同的条件(如环境、配置、类路径是否存在)来决定是否实例化某个模块,或者动态地创建特定类型的实例。
5.1 条件化装配
Spring提供了强大的@Conditional注解及其衍生注解(@Profile,@ConditionalOnClass,@ConditionalOnProperty等),允许我们定义Bean创建的触发条件。
应用场景:
- 多环境配置:开发环境使用内存数据库,生产环境使用MySQL。
@Configuration public class DataSourceConfig { @Bean @Profile("dev") // 仅在dev profile激活时创建 public DataSource inMemoryDataSource() { return new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.H2).build(); } @Bean @Profile("prod") @ConditionalOnProperty(name = "db.type", havingValue = "mysql") public DataSource mysqlDataSource() { // 创建生产环境MySQL数据源 return DataSourceBuilder.create().build(); } } - 类路径依赖:当项目中存在某个类(如某个第三方库)时才启用特定功能模块。
@Configuration @ConditionalOnClass(name = "com.thirdparty.MessageQueueClient") public class MessageQueueAutoConfiguration { @Bean public MessageQueueService messageQueueService() { return new MessageQueueService(); } } - 自定义条件:实现
Condition接口,完成更复杂的判断逻辑。public class ClusterModeCondition implements Condition { @Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { String mode = context.getEnvironment().getProperty("system.mode"); return "cluster".equalsIgnoreCase(mode); } } @Configuration @Conditional(ClusterModeCondition.class) public class ClusterConfiguration { // 集群模式下才需要的Bean }
5.2 动态代理与AOP增强
很多时候,我们需要的不是一个简单的POJO实例,而是一个被增强了功能(如事务、日志、缓存)的代理对象。Spring AOP和动态代理是实现这一点的关键技术。
实例化过程:当Bean被标记了@Transactional,@Cacheable等注解,或匹配了自定义的切面(Aspect)时,容器在完成基本实例化后,会通过BeanPostProcessor介入,使用JDK动态代理或CGLIB字节码增强为目标Bean创建一个代理对象。最终放入容器和应用上下文中的,是这个代理对象。
@Service public class OrderService { @Transactional // 该注解会触发Spring创建事务代理 public Order createOrder(OrderRequest request) { // 业务逻辑 } } // 在另一个Bean中注入的OrderService,实际上是一个代理 @Autowired private OrderService orderService; // 这是一个代理对象,内含事务管理逻辑注意事项:
- 自调用问题:在同一个类中,一个非AOP方法调用另一个有AOP增强的方法(如
@Transactional),增强会失效,因为调用没有经过代理对象。这是AOP基于代理机制的本质决定的。 - 代理方式选择:默认使用JDK动态代理(要求目标类实现接口),如果不实现接口,Spring会使用CGLIB。可以通过
@EnableAspectJAutoProxy(proxyTargetClass = true)强制使用CGLIB。
5.3 懒加载优化启动性能
对于某些启动时非必需、创建成本高昂的模块,可以使用懒加载(Lazy Loading)。容器在启动时不会立即创建它们,只有当第一次被请求(注入或通过ApplicationContext.getBean())时才会初始化。
@Configuration public class HeavyResourceConfig { @Bean @Lazy // 这个Bean不会在应用启动时初始化 public ExpensiveToCreateService expensiveService() { // 模拟耗时很长的初始化过程 Thread.sleep(5000); return new ExpensiveToCreateService(); } }使用场景与权衡:
- 场景:大型报表引擎、复杂的规则计算模块、冷门的第三方服务客户端。
- 优点:显著加快应用启动速度。
- 缺点:第一次请求该Bean时会有延迟,可能影响首个相关请求的响应时间。同时,如果Bean初始化失败,这个错误会延迟到运行时才暴露。
最佳实践:将懒加载与健康检查结合。例如,在Spring Boot Actuator的健康端点中,可以添加一个自定义的健康指示器,在第一次访问时尝试初始化关键懒加载Bean,从而在运维层面提前发现问题。
6. 容器生命周期与模块的生死
模块实例并非一旦创建就永恒存在。理解容器(特别是SpringApplicationContext)的生命周期,以及如何让模块感知并参与到这个生命周期中,对于管理资源(如数据库连接、线程池、文件句柄)至关重要。
6.1 Spring容器的启动与关闭过程
启动:
- 实例化BeanFactory:创建IoC容器的基础设施。
- 加载配置元数据:解析
@Configuration类、XML文件等。 - 实例化Bean(单例、非懒加载):这是核心阶段。容器按照依赖关系,递归地创建所有非懒加载的单例Bean。这个过程会调用
BeanPostProcessor进行增强。 - 初始化Bean:调用
InitializingBean.afterPropertiesSet()或@PostConstruct方法。 - 发布ContextRefreshedEvent事件:容器启动完成,应用进入就绪状态。
运行:处理请求,按需创建原型Bean或懒加载Bean。
关闭(调用
context.close()或收到JVM关闭钩子):- 发布ContextClosedEvent事件。
- 销毁单例Bean:调用
DisposableBean.destroy()或@PreDestroy方法。 - 关闭BeanFactory。
6.2 如何让模块感知生命周期:回调接口与注解
为了让模块能在初始化和销毁时执行特定逻辑(如加载缓存、释放资源),Spring提供了多种方式。
初始化回调:
- 实现
InitializingBean接口(不推荐,与Spring API耦合)。@Component public class CacheManager implements InitializingBean { private Map<String, Object> cache; @Override public void afterPropertiesSet() throws Exception { // 在所有属性被设置后调用 cache = loadDataFromDB(); // 初始化缓存 } } - 使用
@PostConstruct注解(推荐):这是JSR-250标准注解,与Spring解耦。@Component public class CacheManager { private Map<String, Object> cache; @PostConstruct public void init() { cache = loadDataFromDB(); } } - 在
@Bean注解中指定initMethod属性。@Configuration public class AppConfig { @Bean(initMethod = "init") public CacheManager cacheManager() { return new CacheManager(); } } public class CacheManager { public void init() { ... } }
销毁回调: 与初始化对应,有DisposableBean接口、@PreDestroy注解和@Bean(destroyMethod = “close”)。
执行顺序:对于同一个Bean,如果同时使用了多种方式,执行顺序为:构造器 ->@PostConstruct->InitializingBean.afterPropertiesSet()->initMethod。销毁顺序则相反。
6.3 优雅关闭与资源清理实战
在生产环境中,应用的优雅关闭(Graceful Shutdown)至关重要。你需要确保在容器关闭时,所有模块都能有序地释放其占用的资源。
常见需要清理的资源:
- 数据库连接池
- 消息队列消费者连接
- 线程池
- 临时文件锁
- 网络连接(如HTTP客户端、WebSocket连接)
实现方案:
@Component public class MessageQueueConsumer implements DisposableBean { private final ExecutorService executorService = Executors.newFixedThreadPool(5); private volatile boolean running = true; @PostConstruct public void start() { executorService.submit(this::consumeMessages); } private void consumeMessages() { while (running && !Thread.currentThread().isInterrupted()) { // 从消息队列拉取并处理消息 Message msg = queue.poll(); process(msg); } // 循环退出,意味着正在关闭 cleanup(); } @Override public void destroy() throws Exception { running = false; // 1. 设置停止标志 executorService.shutdown(); // 2. 关闭线程池 try { if (!executorService.awaitTermination(30, TimeUnit.SECONDS)) { executorService.shutdownNow(); // 3. 强制关闭 } } catch (InterruptedException e) { executorService.shutdownNow(); Thread.currentThread().interrupt(); } // 4. 关闭消息队列连接等底层资源 closeConnection(); } private void cleanup() { // 执行最后的清理工作,如提交未完成的偏移量 } }配合Spring Boot的优雅关闭: Spring Boot支持在接收到SIGTERM信号时进行优雅关闭。你需要确保:
- 在
application.properties中设置:server.shutdown=graceful和spring.lifecycle.timeout-per-shutdown-phase=30s。 - 如上例所示,你的Bean在
destroy方法中能正确处理中断和超时。 - 对于Web应用,Spring Boot会先停止接收新请求,等待正在处理的请求完成,然后再开始销毁Bean。
踩坑记录:我曾遇到一个因关闭顺序不当导致的数据一致性问题。一个负责写数据库的Service Bean (
ServiceA) 和一个负责发消息通知的Bean (ServiceB) 都实现了DisposableBean。ServiceA的destroy先被调用,关闭了数据库连接。随后ServiceB的destroy被调用,它需要根据数据库中的最终状态发一条消息,但此时数据库连接已关闭,导致异常和消息丢失。解决方案:通过实现SmartLifecycle接口或@DependsOn注解,精确控制Bean的关闭顺序,确保依赖资源的Bean先于被依赖资源的Bean销毁。或者,将关闭逻辑设计成幂等的、不依赖其他可能已关闭的Bean。
