Spring IoC 与 Bean 生命周期
学习目标
- 能区分
@Component、@Service、@Bean三种注册方式,并选择构造器注入。 - 能按实例化、依赖注入、初始化和销毁顺序定位 Bean 生命周期问题。
- 能解释作用域、延迟创建、候选 Bean 和代理对象对运行时行为的影响。
核心知识点
专业术语
- IoC(Inversion of Control):对象的创建、依赖组装和生命周期由容器协调,而不是业务类自己
new依赖。 - Bean 定义(Bean definition):容器创建对象所需的类型、作用域、依赖和初始化元数据。
- 构造器注入(constructor injection):依赖作为构造器参数传入,使对象在构造完成时就满足必需依赖。
- 作用域(scope):Bean 在容器中的实例复用策略,常见有 singleton、prototype、request 和 session。
- 代理对象(proxy):包装目标 Bean 的运行时对象,用于事务、缓存、安全或切面拦截方法调用。
白话解释与边界
IoC 容器像一张对象装配图:先读取 Bean 定义,再按依赖关系创建对象,随后执行初始化回调,最后在容器关闭时销毁可管理资源。构造器注入能让必需依赖显式可见,也更容易写单元测试;可选依赖才应使用 ObjectProvider 等延迟入口。
默认 singleton 是“每个容器一个实例”,不是“整个 JVM 永远一个实例”。prototype Bean 由容器创建但通常不负责后续销毁;把短生命周期对象直接注入长生命周期对象时,要通过 provider 或代理解决生命周期错配。代理只拦截经过代理的调用,自调用通常绕过事务和切面。
版本与兼容基线
本文按 JDK 20 的写法组织示例,使用的 var、record 等语法在 Java 17 已可用,不依赖 JDK 20 之后才出现的 API。项目兼容基线是 Java 17、Spring Boot 4.1.0 与 Spring Framework 7。Spring Boot 4 使用 Jakarta EE 命名空间,示例使用 jakarta.*,它替代旧版 javax.*;迁移旧项目时要以实际依赖版本为准。
常用用法
@Component:注册通用组件
用途:用于把无须显式工厂逻辑的通用类交给组件扫描。
import org.springframework.stereotype.Component;
@Component
// 作用:@Component;注册通用组件。
class ClockSource {
String zone() {
return "UTC";
}
}
System.out.println(new ClockSource().zone());
// 输出:UTC
// 作用:@Component 将 ClockSource 标记为组件扫描候选组件类的包必须在扫描范围内;同类型组件有多个候选时,使用限定符或 @Primary 表达选择,而不是依赖类路径顺序。
@Service:注册服务组件
用途:用于在组件扫描中显式标记承载业务逻辑的服务类。
import org.springframework.stereotype.Service;
@Service
// 作用:@Service;注册服务组件。
class GreetingService {
String greet() {
return "hello";
}
}
System.out.println(new GreetingService().greet());
// 输出:hello
// 作用:@Service 将 GreetingService 标记为业务服务候选@Service 本质上是 @Component 的语义化特殊形式;它不会自动建立事务边界,事务仍需明确配置。
这里的 new GreetingService().greet() 只验证普通方法返回 hello;组件扫描、依赖注入以及可能叠加的事务或缓存代理,只有从 Spring 容器取得该 Bean 时才生效。
@Bean:注册工厂方法结果
用途:用于注册第三方类型、需要组装参数或需要显式生命周期控制的对象;方法返回值就是容器中的 Bean。
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
class ClientConfiguration {
@Bean
// 初始状态:@Bean。
// 作用:@Bean;注册工厂方法结果。
String endpoint() {
return "https://api.example.test";
}
}
System.out.println(new ClientConfiguration().endpoint());
// 输出:https:api.example.test
// 作用:用于注册第三方类型、需要组装参数或需要显式生命周期控制的对象;方法返回值就是容器中的 Bean。@Bean 方法可以声明参数,容器会按类型注入依赖;在 @Configuration 类中方法调用还会由配置类增强代理协调 Bean 复用。普通组件类里的直接方法调用不应被误认为容器查找。
构造器注入:表达必需依赖
用途:用于让对象在构造完成后就处于可用状态,并让依赖关系在类型签名中可见。
import org.springframework.stereotype.Service;
record UserGateway(String endpoint) {}
@Service
class UserService {
private final UserGateway gateway;
UserService(UserGateway gateway) {
this.gateway = gateway;
// 初始状态:gateway = gateway。
}
String endpoint() {
return gateway.endpoint();
// 初始状态:return gateway.endpoint()。
// 作用:return gateway.endpoint();;表达必需依赖。
}
}
System.out.println(new UserService(new UserGateway("users")).endpoint());
// 输出:users
// 作用:用于让对象在构造完成后就处于可用状态,并让依赖关系在类型签名中可见。单构造器通常不需要 @Autowired;必需依赖不要用字段注入隐藏。若两个服务互相需要,构造器注入会在启动时暴露循环依赖,通常应拆分职责或引入事件/端口,而不是随意改成字段注入。
这里手工构造 UserService 只展示构造器把 UserGateway("users") 传入后返回 users;它没有演示 Spring 已完成自动注入,真实注入必须由容器创建并装配 UserService。
@PostConstruct:完成初始化校验
用途:用于依赖注入完成后执行轻量、确定性的初始化;不要在这里启动不可控的长循环或阻塞网络调用。
import jakarta.annotation.PostConstruct;
import org.springframework.stereotype.Component;
@Component
class TokenRules {
private String prefix;
@PostConstruct
// 作用:@PostConstruct;完成初始化校验。
void initialize() {
prefix = "Bearer ";
// 初始状态:prefix = "Bearer "。
System.out.println(prefix.strip());
// 输出:Bearer
}
}
// 作用:@PostConstruct 调用 initialize,把 prefix 初始化为 "Bearer "初始化回调发生在依赖注入之后、应用完全可服务之前;抛出异常通常会让 Bean 创建失败并阻止启动。资源释放对应 @PreDestroy 或显式关闭接口,不能假定所有 prototype Bean 都会收到销毁回调。
作用域:选择实例复用边界
用途:用于根据状态共享范围选择 singleton、prototype 或 Web 作用域;状态型对象必须避免被错误地跨请求共享。
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
@Component
@Scope("prototype")
class RequestDraft {
private int count;
int next() {
return ++count;
}
}
var first = new RequestDraft();
// 初始状态:first = new RequestDraft()。
// 作用:var first = new RequestDraft();;选择实例复用边界。
var second = new RequestDraft();
// 初始状态:second = new RequestDraft()。
// 作用:var second = new RequestDraft();;选择实例复用边界。
System.out.println(first.next() + "/" + second.next());
// 输出:1/1
// 作用:用于根据状态共享范围选择 singleton、prototype 或 Web 作用域;状态型对象必须避免被错误地跨请求共享。prototype 的每次获取语义需要通过容器或 provider 才能体现;直接把它注入 singleton 只会得到一次实例。Web 作用域需要相应的请求上下文,后台线程不能无条件读取 request-scoped Bean。
代理对象:识别被包装的 Bean
用途:用于理解事务、缓存和安全切面为什么依赖代理,以及为什么自调用可能绕过拦截器。
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.AopUtils;
import org.aopalliance.intercept.MethodInterceptor;
interface Greeting {
String text();
}
class GreetingTarget implements Greeting {
public String text() {
return "hello";
}
}
var factory = new ProxyFactory(new GreetingTarget());
// 初始状态:factory = new ProxyFactory(new GreetingTarget())。
// 作用:var factory = new ProxyFactory(new GreetingTarget());;识别被包装的 Bean。
factory.addAdvice((MethodInterceptor) invocation -> invocation.proceed());
// 初始状态:factory.addAdvice((MethodInterceptor) invocation -> invocation.proceed())。
// 作用:factory.addAdvice((MethodInterceptor) invocation -> invocation.proceed());;识别被包装的 Bean。
Greeting proxy = (Greeting) factory.getProxy();
// 初始状态:proxy = (Greeting) factory.getProxy()。
// 作用:Greeting proxy = (Greeting) factory.getProxy();;识别被包装的 Bean,返回读取结果。
System.out.println(AopUtils.isAopProxy(proxy) + "/" + proxy.text());
// 输出:true/hello
// 作用:用于理解事务、缓存和安全切面为什么依赖代理,以及为什么自调用可能绕过拦截器。实际 Spring Bean 可能是 JDK 动态代理或 CGLIB/类代理;注入接口通常更稳妥。代理只能拦截从代理入口进入的方法,类内 this.otherMethod() 不会重新经过代理链。
不常用但需要知道
ObjectProvider:延迟获取与可选依赖
用途:用于表达可选依赖、延迟实例化或按需获取 prototype Bean,避免在构造器中强制创建昂贵对象。
import org.springframework.beans.factory.ObjectProvider;
class OptionalReporter {
OptionalReporter(ObjectProvider<Runnable> provider) {
Runnable task = provider.getIfAvailable(() -> () -> System.out.println("fallback"));
// 输出:fallback
task.run();
// 作用:task.run();;延迟获取与可选依赖。
// 初始状态:task.run()。
}
}
// 作用:ObjectProvider.getIfAvailable 在 provider 缺少 Runnable 时返回 fallbackgetIfAvailable 适合有安全默认值的可选依赖;多个候选时仍会触发歧义异常,应配合 @Primary 或限定符。不要用 provider 掩盖本来应该必需的依赖缺失。
@Lazy:延迟创建 Bean
用途:用于推迟昂贵 Bean 的实例化或打破经过评估的初始化时序,但不能把它当成循环依赖的通用修复。
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
class ExpensiveCatalog {
ExpensiveCatalog() {
System.out.println("constructed-on-getBean");
// 输出:constructed-on-getBean
}
String name() {
return "catalog";
}
}
@Configuration
class LazyConfiguration {
@Bean
@Lazy
// 初始状态:@Lazy。
// 作用:@Lazy;延迟创建 Bean。
ExpensiveCatalog catalog() {
return new ExpensiveCatalog();
// 初始状态:return new ExpensiveCatalog()。
// 作用:return new ExpensiveCatalog();;延迟创建 Bean。
}
}
try (var context = new AnnotationConfigApplicationContext()) {
// 作用:try (var context = new AnnotationConfigApplicationContext()) {;延迟创建 Bean。
// 初始状态:context = new AnnotationConfigApplicationContext())。
context.register(LazyConfiguration.class);
// 作用:context.register(LazyConfiguration.class);;延迟创建 Bean。
context.refresh();
// 作用:context.refresh();;延迟创建 Bean。
System.out.println("refreshed");
// 输出:refreshed
System.out.println("lazy=" + context.getBean(ExpensiveCatalog.class).name());
// 输出:lazy=catalog
}
// 作用:用于推迟昂贵 Bean 的实例化或打破经过评估的初始化时序,但不能把它当成循环依赖的通用修复。懒加载把失败从启动期推迟到第一次访问,排障和健康检查会更复杂。对关键 Bean,应权衡启动速度、首请求延迟和失败可见性,并为第一次创建提供监控。
@Primary:声明默认候选
用途:用于同一接口存在多个实现时指定默认注入对象;只有一个候选需要时才使用,复杂场景改用限定符更清晰。
// 初始状态:@Primary 标记 systemClockNotifier,容器中另有 backupClockNotifier
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
interface Notifier {
String channel();
}
class AlertService {
private final Notifier notifier;
AlertService(Notifier notifier) {
this.notifier = notifier;
// 初始状态:notifier = notifier。
}
String selectedChannel() {
return notifier.channel();
// 作用:return notifier.channel();;声明默认候选。
}
}
@Configuration
class PrimaryConfiguration {
@Bean
@Primary
// 作用:@Primary;声明默认候选。
Notifier systemClockNotifier() {
return () -> "system-clock";
}
@Bean
Notifier backupClockNotifier() {
return () -> "backup-clock";
}
@Bean
AlertService alertService(Notifier notifier) {
return new AlertService(notifier);
// 作用:return new AlertService(notifier);;声明默认候选。
// 初始状态:return new AlertService(notifier)。
}
}
try (var context = new AnnotationConfigApplicationContext(PrimaryConfiguration.class)) {
// 作用:try (var context = new AnnotationConfigApplicationContext(PrimaryConfiguration.class)) {;声明默认候选。
// 初始状态:context = new AnnotationConfigApplicationContext(PrimaryConfiguration.class))。
System.out.println(context.getBean(Notifier.class).channel());
// 输出:system-clock
System.out.println(context.getBean(AlertService.class).selectedChannel());
// 输出:system-clock(第2次输出)
}@Primary 只影响容器按类型选择:上例既验证 getBean(Notifier.class) 的解析,也验证构造器参数选择。直接 new 某个实现会绕过 Spring 候选解析,不能证明 @Primary 生效;它也不会改变 Bean 名称或覆盖显式限定符。
常用调用标题补齐
@Autowired:旧代码的按类型注入
@Autowired 按类型解析依赖;新代码优先用单构造器注入,避免隐藏必需依赖。
// 作用:通过 @Autowired 旧代码的按类型注入。
// 结果:`now()` 使用容器中的 `Clock` Bean 返回当前时刻。
@Service
class BillingService {
private final Clock clock;
@Autowired
// 作用:@Autowired;旧代码的按类型注入。
BillingService(Clock clock) {
this.clock = clock;
// 初始状态:clock = clock。
}
Instant now() { return clock.instant(); }
// 作用:Instant now() { return clock.instant(); };旧代码的按类型注入。
// 初始状态:Instant now() { return clock.instant(); 。
}输出:now() 使用容器中的 Clock Bean 返回当前时刻。
@Value:注入单个配置值
@Value 适合少量标量配置;成组配置应改用 @ConfigurationProperties 以获得类型校验。
// 作用:通过 @Value 注入单个配置值。
// 结果:未配置时,`accepts(1024)` 返回 `true`。
@Component
class UploadLimits {
private final long maxBytes;
UploadLimits(@Value("${app.upload.max-bytes:10485760}") long maxBytes) {
// 作用:UploadLimits(@Value("${app.upload.max-bytes:10485760}") long maxBytes) {;注入单个配置值。
this.maxBytes = maxBytes;
// 初始状态:maxBytes = maxBytes。
}
boolean accepts(long size) {
return size <= maxBytes;
}
}输出:未配置时,accepts(1024) 返回 true。
@ConditionalOnProperty:按开关装配 Bean
@ConditionalOnProperty 只决定 Bean 是否注册;不要把它当成运行期功能开关。
// 作用:通过 @ConditionalOnProperty 按开关装配 Bean。
@Configuration
class AuditConfiguration {
@Bean
@ConditionalOnProperty(name = "app.audit.enabled", havingValue = "true")
// 初始状态:name = "app.audit.enabled", havingValue = "true")。
// 作用:@ConditionalOnProperty(name = "app.audit.enabled", havingValue = "true");按开关装配 Bean。
AuditSink auditSink() {
return event -> System.out.println(event);
// 输出:仅当 `app.audit.enabled=true` 时容器中存在 `AuditSink`。
}
interface AuditSink { void write(String event); }
}输出:仅当 app.audit.enabled=true 时容器中存在 AuditSink。
SpringApplication.run:启动并取得容器
SpringApplication.run 创建并刷新 Spring 容器;启动参数会进入外部配置优先级链。
// 作用:通过 SpringApplication.run 启动并取得容器。
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
ConfigurableApplicationContext context =
SpringApplication.run(DemoApplication.class, args);
// 初始状态:SpringApplication.run(DemoApplication.class, args)。
// 作用:SpringApplication.run(DemoApplication.class, args);;启动并取得容器。
String id = context.getId();
// 作用:String id = context.getId();;启动并取得容器。
// 初始状态:id = context.getId()。
System.out.println(id);
// 输出:打印已启动容器的 ID,例如 `application`。
}
}输出:打印已启动容器的 ID,例如 application。
FilterRegistrationBean:以 Bean 方式注册 Servlet Filter
FilterRegistrationBean 用于非 Spring Security 的 Servlet Filter;安全链内的过滤器应由 SecurityFilterChain 排序。
// 作用:通过 FilterRegistrationBean 以 Bean 方式注册 Servlet Filter。
// 结果:容器以顺序 `10` 注册 `RequestIdFilter`。
@Configuration
class FilterConfiguration {
@Bean
FilterRegistrationBean<RequestIdFilter> requestIdFilter() {
FilterRegistrationBean<RequestIdFilter> bean =
new FilterRegistrationBean<>(new RequestIdFilter());
// 初始状态:new FilterRegistrationBean<>(new RequestIdFilter())。
// 作用:new FilterRegistrationBean<>(new RequestIdFilter());;以 Bean 方式注册 Servlet Filter。
bean.setOrder(10);
// 作用:bean.setOrder(10);;以 Bean 方式注册 Servlet Filter。
return bean;
}
}输出:容器以顺序 10 注册 RequestIdFilter。
FilterRegistrationBean.addUrlPatterns:限制 Filter 映射
addUrlPatterns 接收 Servlet URL pattern,不是 Spring MVC 的 Ant 路径规则。
// 作用:通过 FilterRegistrationBean.addUrlPatterns 限制 Filter 映射。
// 结果:`RequestIdFilter` 只匹配 `/api/*` 的 Servlet 请求。
@Bean
FilterRegistrationBean<RequestIdFilter> apiFilter() {
FilterRegistrationBean<RequestIdFilter> bean =
new FilterRegistrationBean<>();
// 作用:new FilterRegistrationBean<>();;限制 Filter 映射。
// 初始状态:new FilterRegistrationBean<>()。
bean.setFilter(new RequestIdFilter());
// 作用:bean.setFilter(new RequestIdFilter());;限制 Filter 映射。
// 初始状态:bean.setFilter(new RequestIdFilter())。
bean.addUrlPatterns("/api/*");
// 初始状态:bean.addUrlPatterns("/api/*")。
// 作用:bean.addUrlPatterns("/api/*");;限制 Filter 映射。
bean.setName("requestIdFilter");
// 作用:bean.setName("requestIdFilter");;限制 Filter 映射。
return bean;
}输出:RequestIdFilter 只匹配 /api/* 的 Servlet 请求。
@EnableCaching:开启 Spring 缓存代理
@EnableCaching 启用 @Cacheable 等注解的代理处理;同类自调用仍会绕过代理。
// 作用:通过 @EnableCaching 开启 Spring 缓存代理。
// 结果:容器启用缓存切面,并提供名为 `users` 的缓存。
@Configuration
@EnableCaching
// 作用:@EnableCaching;开启 Spring 缓存代理。
class CacheConfiguration {
@Bean
CacheManager cacheManager() {
return new ConcurrentMapCacheManager("users");
// 初始状态:return new ConcurrentMapCacheManager("users")。
// 作用:return new ConcurrentMapCacheManager("users");;开启 Spring 缓存代理。
}
String configuredCache() { return "users"; }
}输出:容器启用缓存切面,并提供名为 users 的缓存。
@PropertySource:引入额外 properties 资源
@PropertySource 适合补充 .properties 文件;它不直接支持 YAML,也不应覆盖 Boot 的常规配置约定。
// 作用:通过 @PropertySource 引入额外 properties 资源。
// 结果:未配置 `gen.author` 时,`author()` 返回 `team`。
@Configuration
@PropertySource("classpath:generator.properties")
// 作用:@PropertySource("classpath:generator.properties");引入额外 properties 资源。
class GeneratorConfiguration {
private final Environment environment;
GeneratorConfiguration(Environment environment) {
this.environment = environment;
// 初始状态:environment = environment。
}
String author() { return environment.getProperty("gen.author", "team"); }
// 作用:String author() { return environment.getProperty("gen.author", "team"); };引入额外 properties 资源。
// 初始状态:String author() { return environment.getProperty("gen.author", "team"); 。
}输出:未配置 gen.author 时,author() 返回 team。
@Resource:按名称优先注入
@Resource 默认先按名称匹配,适合需要明确 Bean 名的旧代码;必需依赖仍优先构造器注入。
// 作用:通过 @Resource 按名称优先注入。
// 结果:容器按名称注入 `stringRedisTemplate`,`ready()` 返回 `true`。
@Component
class CaptchaFacade {
@Resource(name = "stringRedisTemplate")
// 初始状态:name = "stringRedisTemplate")。
// 作用:@Resource(name = "stringRedisTemplate");按名称优先注入。
private StringRedisTemplate redis;
void save(String key, String value) {
redis.opsForValue().set(key, value);
// 作用:redis.opsForValue().set(key, value);;按名称优先注入。
}
boolean ready() { return redis != null; }
}输出:容器按名称注入 stringRedisTemplate,ready() 返回 true。
继续阅读
- List 基础:容器注入多个实现时,先查
List的顺序、可变性和快照边界。 - Map 基础:按 Bean 名称收集多个实现时,查
Map的键和值视图语义。 - String 文本处理:处理 Bean 名称、限定符和配置键时,查字符串比较与规范化。
- 异常处理常用写法:初始化失败和依赖缺失需要统一异常边界时参考。
简单案例
import jakarta.annotation.PostConstruct;
import org.springframework.stereotype.Service;
record Profile(String name) {}
@Service
class ProfileService {
private final Profile profile;
ProfileService(Profile profile) {
this.profile = profile;
// 初始状态:profile = profile。
// 作用:this.profile = profile;;按名称优先注入。
}
@PostConstruct
void check() {
if (profile.name().isBlank()) {
throw new IllegalStateException("profile name is blank");
// 初始状态:throw new IllegalStateException("profile name is blank")。
// 作用:throw new IllegalStateException("profile name is blank");;按名称优先注入。
}
}
String name() {
return profile.name();
// 作用:return profile.name();;按名称优先注入。
}
}
System.out.println(new ProfileService(new Profile("dev")).name());
// 输出:dev这里的 new ProfileService(new Profile("dev")) 只验证普通构造器和 name() 返回 dev;@PostConstruct 等生命周期回调不会因手工 new 自动执行。真正的 Spring 应用还要通过 @Bean 或配置属性提供 Profile,并由容器创建 ProfileService。如果依赖是按请求变化的状态,应使用正确作用域或 provider,不能把它塞进 singleton 服务的可变字段。
易混点
框架片段需容器运行:生命周期回调、条件装配和代理行为都由 Spring 容器驱动,单独运行片段不能复现完整时序;测试时应启动最小应用上下文并断言 Bean 状态。
@Component/@Service依赖组件扫描,@Bean依赖配置方法;两者都注册 Bean,但表达意图不同。- singleton 是容器级复用,不是线程安全保证;可变字段仍需并发设计。
- prototype 只保证获取时创建新实例,容器通常不替你完成完整销毁流程。
- 注入代理后调用经过代理的方法才会触发切面;同类
this调用通常绕过代理。 @Lazy可以推迟失败,不能自动解决设计上的循环依赖;构造器依赖环应优先拆分职责。
课后小问
为什么构造器注入有助于发现循环依赖? 答案:容器必须在构造对象前提供全部构造器参数,互相等待的依赖环会在启动阶段明确失败。 解析:这比隐藏在字段注入后的半初始化对象更早暴露设计问题,通常应拆出共同职责或改为事件协作。
singleton Bean 一定线程安全吗? 答案:不一定,singleton 只描述实例复用范围,不会自动保护可变字段。 解析:并发请求会共享同一实例;状态应尽量保持无状态,必须共享时要采用明确的并发控制。
为什么自调用可能绕过
@Transactional或切面? 答案:自调用使用目标对象的this引用,没有从 Spring 代理对象入口经过拦截器链。 解析:可拆分到另一个 Bean、从外部代理调用,或谨慎使用其他设计;不要仅靠注解文字推断拦截已经发生。
本节小结
- IoC 容器负责 Bean 定义、依赖组装、初始化和可管理资源的销毁。
- 构造器注入让必需依赖显式可见,
ObjectProvider只应表达可选或延迟依赖。 - 作用域描述实例生命周期,singleton 不等于线程安全,prototype 也不等于自动销毁。
- 代理支撑事务、缓存和安全拦截,但自调用和直接
new都可能绕开容器能力。
快速回顾
- 能为普通组件、第三方对象和多个候选选择
@Component、@Bean或限定符。 - 能按实例化、注入、初始化和销毁顺序解释 Bean 生命周期。
- 能说明 singleton、prototype、request 作用域的共享边界。
- 能识别代理对象、自调用和循环依赖造成的常见误判。
阿源的知识库