diff --git a/src/main/java/com/example/scaffold/learning/IocLearningController.java b/src/main/java/com/example/scaffold/learning/IocLearningController.java index 553b398..ce260d6 100644 --- a/src/main/java/com/example/scaffold/learning/IocLearningController.java +++ b/src/main/java/com/example/scaffold/learning/IocLearningController.java @@ -1,10 +1,10 @@ package com.example.scaffold.learning; import com.example.scaffold.aop.PerformanceAspect; +import com.example.scaffold.learning.lifecycle.IocLifecycleTracker; +import com.example.scaffold.learning.lifecycle.LifecycleDemoBean; import lombok.RequiredArgsConstructor; import org.springframework.beans.BeansException; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Scope; import org.springframework.web.bind.annotation.*; @@ -30,6 +30,7 @@ public class IocLearningController { private final ApplicationContext applicationContext; private final PerformanceAspect performanceAspect; + private final IocLifecycleTracker lifecycleTracker; // 演示字段注入(不推荐,但可以用) // 注意:session scope 的 bean 需要在 web 上下文中使用 @@ -99,10 +100,77 @@ public class IocLearningController { "prototype", "原型 - 每次请求都创建新实例", "request", "请求 - 每个 HTTP 请求一个实例", "session", "会话 - 每个 HTTP 会话一个实例", + "lazySingleton", "懒加载单例 - 第一次真正获取时才创建", "tip", "session scope 需要在 web 请求上下文中使用" ); } + /** + * IoC 生命周期总览 + */ + @GetMapping("/lifecycle/overview") + public Map lifecycleOverview() { + return Map.of( + "jvmClassLoading", List.of( + "类第一次被引用时才可能触发加载", + "static 代码块在类初始化阶段执行一次", + "类加载成功后,JVM 里就有这个 Class 元数据" + ), + "springBeanLifecycle", List.of( + "扫描 BeanDefinition", + "实例化 Bean(singleton 通常在启动阶段)", + "依赖注入", + "@PostConstruct 初始化", + "放入单例池或按 scope 管理", + "容器关闭时销毁 singleton Bean" + ), + "coreIdea", "类加载 ≠ Bean 创建;Bean 创建 ≠ 每次请求都 new" + ); + } + + @GetMapping("/lifecycle/timeline") + public List> lifecycleTimeline() { + return lifecycleTracker.snapshot(); + } + + @PostMapping("/lifecycle/reset") + public Map resetLifecycleTimeline() { + lifecycleTracker.reset(); + return Map.of("status", "ok", "message", "生命周期时间线已重置"); + } + + @GetMapping("/lifecycle/inspect/{scopeType}") + public Map inspectLifecycleBean(@PathVariable String scopeType, + @RequestParam(defaultValue = "manual-check") String trigger) { + String beanName = switch (scopeType) { + case "prototype" -> "iocPrototypeLifecycleBean"; + case "lazy" -> "iocLazySingletonLifecycleBean"; + default -> "iocSingletonLifecycleBean"; + }; + LifecycleDemoBean bean = (LifecycleDemoBean) applicationContext.getBean(beanName); + Map result = new LinkedHashMap<>(bean.inspect(trigger)); + result.put("beanNameFromContext", beanName); + result.put("isSingletonInContext", applicationContext.isSingleton(beanName)); + result.put("timelineSize", lifecycleTracker.snapshot().size()); + return result; + } + + @GetMapping("/lifecycle/compare") + public Map compareScopes() { + LifecycleDemoBean singleton1 = (LifecycleDemoBean) applicationContext.getBean("iocSingletonLifecycleBean"); + LifecycleDemoBean singleton2 = (LifecycleDemoBean) applicationContext.getBean("iocSingletonLifecycleBean"); + LifecycleDemoBean prototype1 = (LifecycleDemoBean) applicationContext.getBean("iocPrototypeLifecycleBean"); + LifecycleDemoBean prototype2 = (LifecycleDemoBean) applicationContext.getBean("iocPrototypeLifecycleBean"); + LifecycleDemoBean lazy1 = (LifecycleDemoBean) applicationContext.getBean("iocLazySingletonLifecycleBean"); + LifecycleDemoBean lazy2 = (LifecycleDemoBean) applicationContext.getBean("iocLazySingletonLifecycleBean"); + + return Map.of( + "singleton", List.of(singleton1.inspect("compare-1"), singleton2.inspect("compare-2")), + "prototype", List.of(prototype1.inspect("compare-1"), prototype2.inspect("compare-2")), + "lazySingleton", List.of(lazy1.inspect("compare-1"), lazy2.inspect("compare-2")) + ); + } + /** * 性能统计 */ diff --git a/src/main/java/com/example/scaffold/learning/lifecycle/IocLifecycleTracker.java b/src/main/java/com/example/scaffold/learning/lifecycle/IocLifecycleTracker.java new file mode 100644 index 0000000..472d476 --- /dev/null +++ b/src/main/java/com/example/scaffold/learning/lifecycle/IocLifecycleTracker.java @@ -0,0 +1,39 @@ +package com.example.scaffold.learning.lifecycle; + +import org.springframework.stereotype.Component; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicLong; + +@Component +public class IocLifecycleTracker { + + private static final AtomicLong SEQ = new AtomicLong(0); + private static final CopyOnWriteArrayList> EVENTS = new CopyOnWriteArrayList<>(); + + public static void record(String beanName, String scope, String phase, String detail) { + Map event = new LinkedHashMap<>(); + event.put("seq", SEQ.incrementAndGet()); + event.put("time", Instant.now().toString()); + event.put("beanName", beanName); + event.put("scope", scope); + event.put("phase", phase); + event.put("detail", detail); + EVENTS.add(event); + } + + public List> snapshot() { + return new ArrayList<>(EVENTS); + } + + public void reset() { + EVENTS.clear(); + SEQ.set(0); + record("system", "n/a", "reset", "实验日志已重置"); + } +} diff --git a/src/main/java/com/example/scaffold/learning/lifecycle/LazySingletonLifecycleBean.java b/src/main/java/com/example/scaffold/learning/lifecycle/LazySingletonLifecycleBean.java new file mode 100644 index 0000000..617d4d4 --- /dev/null +++ b/src/main/java/com/example/scaffold/learning/lifecycle/LazySingletonLifecycleBean.java @@ -0,0 +1,51 @@ +package com.example.scaffold.learning.lifecycle; + +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; + +@Component("iocLazySingletonLifecycleBean") +@Lazy +public class LazySingletonLifecycleBean implements LifecycleDemoBean { + + static { + IocLifecycleTracker.record("iocLazySingletonLifecycleBean", "lazy-singleton", "class-load", "JVM 初始化类(static block)"); + } + + private final String instanceId = UUID.randomUUID().toString().substring(0, 8); + private final AtomicInteger accessCount = new AtomicInteger(0); + + public LazySingletonLifecycleBean() { + IocLifecycleTracker.record("iocLazySingletonLifecycleBean", "lazy-singleton", "constructor", "构造器执行,说明第一次真正获取才创建实例"); + } + + @PostConstruct + public void init() { + IocLifecycleTracker.record("iocLazySingletonLifecycleBean", "lazy-singleton", "post-construct", "Lazy 单例初始化完成,并被放入单例缓存"); + } + + @PreDestroy + public void destroy() { + IocLifecycleTracker.record("iocLazySingletonLifecycleBean", "lazy-singleton", "pre-destroy", "容器关闭时销毁 lazy singleton"); + } + + @Override + public Map inspect(String trigger) { + int count = accessCount.incrementAndGet(); + IocLifecycleTracker.record("iocLazySingletonLifecycleBean", "lazy-singleton", "method-call", "trigger=" + trigger + ", accessCount=" + count); + Map result = new LinkedHashMap<>(); + result.put("beanName", "iocLazySingletonLifecycleBean"); + result.put("scope", "lazy-singleton"); + result.put("instanceId", instanceId); + result.put("identityHashCode", System.identityHashCode(this)); + result.put("accessCount", count); + result.put("explanation", "Lazy 单例不会在容器启动时创建,而是在第一次 getBean 时才真正实例化"); + return result; + } +} diff --git a/src/main/java/com/example/scaffold/learning/lifecycle/LifecycleDemoBean.java b/src/main/java/com/example/scaffold/learning/lifecycle/LifecycleDemoBean.java new file mode 100644 index 0000000..8ed1181 --- /dev/null +++ b/src/main/java/com/example/scaffold/learning/lifecycle/LifecycleDemoBean.java @@ -0,0 +1,7 @@ +package com.example.scaffold.learning.lifecycle; + +import java.util.Map; + +public interface LifecycleDemoBean { + Map inspect(String trigger); +} diff --git a/src/main/java/com/example/scaffold/learning/lifecycle/PrototypeLifecycleBean.java b/src/main/java/com/example/scaffold/learning/lifecycle/PrototypeLifecycleBean.java new file mode 100644 index 0000000..0d84554 --- /dev/null +++ b/src/main/java/com/example/scaffold/learning/lifecycle/PrototypeLifecycleBean.java @@ -0,0 +1,51 @@ +package com.example.scaffold.learning.lifecycle; + +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; + +@Component("iocPrototypeLifecycleBean") +@Scope("prototype") +public class PrototypeLifecycleBean implements LifecycleDemoBean { + + static { + IocLifecycleTracker.record("iocPrototypeLifecycleBean", "prototype", "class-load", "JVM 初始化类(static block,仅第一次)"); + } + + private final String instanceId = UUID.randomUUID().toString().substring(0, 8); + private final AtomicInteger accessCount = new AtomicInteger(0); + + public PrototypeLifecycleBean() { + IocLifecycleTracker.record("iocPrototypeLifecycleBean", "prototype", "constructor", "构造器执行,实例ID=" + instanceId); + } + + @PostConstruct + public void init() { + IocLifecycleTracker.record("iocPrototypeLifecycleBean", "prototype", "post-construct", "原型 Bean 初始化完成"); + } + + @PreDestroy + public void destroy() { + IocLifecycleTracker.record("iocPrototypeLifecycleBean", "prototype", "pre-destroy", "注意:prototype Bean 默认不会由容器统一销毁"); + } + + @Override + public Map inspect(String trigger) { + int count = accessCount.incrementAndGet(); + IocLifecycleTracker.record("iocPrototypeLifecycleBean", "prototype", "method-call", "trigger=" + trigger + ", accessCount=" + count); + Map result = new LinkedHashMap<>(); + result.put("beanName", "iocPrototypeLifecycleBean"); + result.put("scope", "prototype"); + result.put("instanceId", instanceId); + result.put("identityHashCode", System.identityHashCode(this)); + result.put("accessCount", count); + result.put("explanation", "原型 Bean 每次获取都会创建新实例,因此实例 ID / hashCode 会变化"); + return result; + } +} diff --git a/src/main/java/com/example/scaffold/learning/lifecycle/SingletonLifecycleBean.java b/src/main/java/com/example/scaffold/learning/lifecycle/SingletonLifecycleBean.java new file mode 100644 index 0000000..a934324 --- /dev/null +++ b/src/main/java/com/example/scaffold/learning/lifecycle/SingletonLifecycleBean.java @@ -0,0 +1,49 @@ +package com.example.scaffold.learning.lifecycle; + +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import org.springframework.stereotype.Component; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; + +@Component("iocSingletonLifecycleBean") +public class SingletonLifecycleBean implements LifecycleDemoBean { + + static { + IocLifecycleTracker.record("iocSingletonLifecycleBean", "singleton", "class-load", "JVM 初始化类(static block)"); + } + + private final String instanceId = UUID.randomUUID().toString().substring(0, 8); + private final AtomicInteger accessCount = new AtomicInteger(0); + + public SingletonLifecycleBean() { + IocLifecycleTracker.record("iocSingletonLifecycleBean", "singleton", "constructor", "构造器执行,实例ID=" + instanceId); + } + + @PostConstruct + public void init() { + IocLifecycleTracker.record("iocSingletonLifecycleBean", "singleton", "post-construct", "Bean 初始化完成,已进入单例生命周期"); + } + + @PreDestroy + public void destroy() { + IocLifecycleTracker.record("iocSingletonLifecycleBean", "singleton", "pre-destroy", "容器关闭时销毁单例 Bean"); + } + + @Override + public Map inspect(String trigger) { + int count = accessCount.incrementAndGet(); + IocLifecycleTracker.record("iocSingletonLifecycleBean", "singleton", "method-call", "trigger=" + trigger + ", accessCount=" + count); + Map result = new LinkedHashMap<>(); + result.put("beanName", "iocSingletonLifecycleBean"); + result.put("scope", "singleton"); + result.put("instanceId", instanceId); + result.put("identityHashCode", System.identityHashCode(this)); + result.put("accessCount", count); + result.put("explanation", "单例 Bean 在容器中通常只创建一次,后续每次获取都是同一个实例"); + return result; + } +} diff --git a/src/main/resources/static/ioc.html b/src/main/resources/static/ioc.html index 473f3f1..8dc0fe4 100644 --- a/src/main/resources/static/ioc.html +++ b/src/main/resources/static/ioc.html @@ -3,173 +3,200 @@ - IoC 容器学习 - Spring Boot + IoC 生命周期可视化实验室 - Spring Boot
-

📦 IoC 容器学习

-

控制反转 (Inversion of Control) 与依赖注入 (Dependency Injection)

+

📦 IoC 生命周期可视化实验室

+

把“类加载”“Bean 创建”“单例/多例/懒加载”拆开看,自己动手触发、自己观察结果。

- + - -
-

📚 核心概念

-
-

什么是 IoC?

-

控制反转 (Inversion of Control):将对象的创建和管理交给 Spring 容器,而不是由开发者手动创建。

-

依赖注入 (Dependency Injection):IoC 的一种实现方式,通过构造器、Setter 或字段将依赖注入到对象中。

+ +
+ 🧪 实验任务卡 +
    +
  • 先点“加载总览”,搞清楚 JVM 类加载 和 Spring Bean 生命周期不是一回事
  • +
  • 再点“比较三种作用域”,对比 singleton / prototype / lazy singleton 的实例 ID 和 hashCode
  • +
  • 然后分别连续点击“获取单例 / 获取多例 / 获取懒加载单例”,观察时间线变化
  • +
  • 最后重置时间线,再重新做一遍,看看第一次触发和第二次触发的区别
  • +
+
+ +
+
+

📚 核心概念总览

+ +
点击按钮查看“类加载 vs Bean 生命周期”...
-
-

为什么用 IoC?

-
    -
  • 解耦:对象之间不直接依赖,通过接口交互
  • -
  • 可测试:方便使用 Mock 对象进行单元测试
  • -
  • 可维护:集中管理对象生命周期
  • -
  • AOP 支持:便于实现切面编程
  • -
+ +
+

🆚 作用域对比实验

+ + +
观察重点:singleton 两次获取是否同一实例?prototype 为什么每次都变?lazy singleton 第一次获取前是否就已经创建?
+
- + +
+

🎯 单点触发实验

+ + + +
点击上面的按钮,观察实例 ID / hashCode / 访问次数如何变化...
+
+ +
+
+

🕰️ 生命周期时间线

+ +
时间线加载中...
+
+ +
+

📊 Bean 作用域解释

+ + + + + +
作用域创建时机实例特点
singleton通常容器启动时全局一个实例,反复获取同一个对象
prototype每次 getBean 时每次都是新实例,不进单例池
lazy singleton第一次真正使用时启动不创建,首次获取才创建,之后复用
+
+

记住这句话

+

类加载 ≠ Bean 创建;Bean 创建 ≠ 每次请求都 new。

+
+
+
+

🔍 查看所有 Bean

-

Spring 容器中管理的所有 Bean 对象

- -
-

📊 Bean 作用域

- - - - - - -
作用域说明使用场景
singleton默认,整个应用只有一个实例无状态的服务、配置类
prototype每次请求都创建新实例有状态的对象
request每个 HTTP 请求一个实例Web 应用
session每个 HTTP 会话一个实例用户会话数据
- -
-
- -
-

⚡ 性能统计

-

实时监控方法执行时间和调用次数

- - -
-
- -
-

💉 依赖注入方式对比

- - - - - -
方式优点缺点推荐度
构造器注入明确依赖、不可变、易测试参数多时代码长⭐⭐⭐⭐⭐
Setter 注入可选依赖、灵活可能为 null⭐⭐⭐
字段注入代码简洁隐藏依赖、难测试
-
- + - \ No newline at end of file +