feat(ioc): add lifecycle visualization lab for class loading and bean scopes
This commit is contained in:
@@ -1,10 +1,10 @@
|
|||||||
package com.example.scaffold.learning;
|
package com.example.scaffold.learning;
|
||||||
|
|
||||||
import com.example.scaffold.aop.PerformanceAspect;
|
import com.example.scaffold.aop.PerformanceAspect;
|
||||||
|
import com.example.scaffold.learning.lifecycle.IocLifecycleTracker;
|
||||||
|
import com.example.scaffold.learning.lifecycle.LifecycleDemoBean;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.beans.BeansException;
|
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.ApplicationContext;
|
||||||
import org.springframework.context.annotation.Scope;
|
import org.springframework.context.annotation.Scope;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
@@ -30,6 +30,7 @@ public class IocLearningController {
|
|||||||
|
|
||||||
private final ApplicationContext applicationContext;
|
private final ApplicationContext applicationContext;
|
||||||
private final PerformanceAspect performanceAspect;
|
private final PerformanceAspect performanceAspect;
|
||||||
|
private final IocLifecycleTracker lifecycleTracker;
|
||||||
|
|
||||||
// 演示字段注入(不推荐,但可以用)
|
// 演示字段注入(不推荐,但可以用)
|
||||||
// 注意:session scope 的 bean 需要在 web 上下文中使用
|
// 注意:session scope 的 bean 需要在 web 上下文中使用
|
||||||
@@ -99,10 +100,77 @@ public class IocLearningController {
|
|||||||
"prototype", "原型 - 每次请求都创建新实例",
|
"prototype", "原型 - 每次请求都创建新实例",
|
||||||
"request", "请求 - 每个 HTTP 请求一个实例",
|
"request", "请求 - 每个 HTTP 请求一个实例",
|
||||||
"session", "会话 - 每个 HTTP 会话一个实例",
|
"session", "会话 - 每个 HTTP 会话一个实例",
|
||||||
|
"lazySingleton", "懒加载单例 - 第一次真正获取时才创建",
|
||||||
"tip", "session scope 需要在 web 请求上下文中使用"
|
"tip", "session scope 需要在 web 请求上下文中使用"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IoC 生命周期总览
|
||||||
|
*/
|
||||||
|
@GetMapping("/lifecycle/overview")
|
||||||
|
public Map<String, Object> 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<Map<String, Object>> lifecycleTimeline() {
|
||||||
|
return lifecycleTracker.snapshot();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/lifecycle/reset")
|
||||||
|
public Map<String, String> resetLifecycleTimeline() {
|
||||||
|
lifecycleTracker.reset();
|
||||||
|
return Map.of("status", "ok", "message", "生命周期时间线已重置");
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/lifecycle/inspect/{scopeType}")
|
||||||
|
public Map<String, Object> 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<String, Object> 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<String, Object> 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"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 性能统计
|
* 性能统计
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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<Map<String, Object>> EVENTS = new CopyOnWriteArrayList<>();
|
||||||
|
|
||||||
|
public static void record(String beanName, String scope, String phase, String detail) {
|
||||||
|
Map<String, Object> 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<Map<String, Object>> snapshot() {
|
||||||
|
return new ArrayList<>(EVENTS);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reset() {
|
||||||
|
EVENTS.clear();
|
||||||
|
SEQ.set(0);
|
||||||
|
record("system", "n/a", "reset", "实验日志已重置");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<String, Object> inspect(String trigger) {
|
||||||
|
int count = accessCount.incrementAndGet();
|
||||||
|
IocLifecycleTracker.record("iocLazySingletonLifecycleBean", "lazy-singleton", "method-call", "trigger=" + trigger + ", accessCount=" + count);
|
||||||
|
Map<String, Object> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.example.scaffold.learning.lifecycle;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public interface LifecycleDemoBean {
|
||||||
|
Map<String, Object> inspect(String trigger);
|
||||||
|
}
|
||||||
@@ -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<String, Object> inspect(String trigger) {
|
||||||
|
int count = accessCount.incrementAndGet();
|
||||||
|
IocLifecycleTracker.record("iocPrototypeLifecycleBean", "prototype", "method-call", "trigger=" + trigger + ", accessCount=" + count);
|
||||||
|
Map<String, Object> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<String, Object> inspect(String trigger) {
|
||||||
|
int count = accessCount.incrementAndGet();
|
||||||
|
IocLifecycleTracker.record("iocSingletonLifecycleBean", "singleton", "method-call", "trigger=" + trigger + ", accessCount=" + count);
|
||||||
|
Map<String, Object> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,173 +3,200 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>IoC 容器学习 - Spring Boot</title>
|
<title>IoC 生命周期可视化实验室 - Spring Boot</title>
|
||||||
<style>
|
<style>
|
||||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; }
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; }
|
||||||
.container { max-width: 1400px; margin: 0 auto; padding: 20px; }
|
.container { max-width: 1400px; margin: 0 auto; padding: 20px; }
|
||||||
|
|
||||||
.header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 30px 20px; text-align: center; margin-bottom: 20px; border-radius: 10px; }
|
.header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 30px 20px; text-align: center; margin-bottom: 20px; border-radius: 10px; }
|
||||||
.header h1 { font-size: 2em; }
|
.header h1 { font-size: 2em; }
|
||||||
|
|
||||||
.nav { display: flex; gap: 10px; margin-bottom: 20px; flex-wrap: wrap; justify-content: center; }
|
.nav { display: flex; gap: 10px; margin-bottom: 20px; flex-wrap: wrap; justify-content: center; }
|
||||||
.nav a { padding: 10px 20px; background: white; border-radius: 20px; text-decoration: none; color: #333; font-size: 0.9em; }
|
.nav a { padding: 10px 20px; background: white; border-radius: 20px; text-decoration: none; color: #333; font-size: 0.9em; }
|
||||||
.nav a:hover, .nav a.active { background: #667eea; color: white; }
|
.nav a:hover, .nav a.active { background: #667eea; color: white; }
|
||||||
|
|
||||||
.card { background: white; border-radius: 10px; padding: 20px; margin-bottom: 20px; box-shadow: 0 2px 10px rgba(0,0,0,0.08); }
|
.card { background: white; border-radius: 10px; padding: 20px; margin-bottom: 20px; box-shadow: 0 2px 10px rgba(0,0,0,0.08); }
|
||||||
.card h3 { color: #667eea; margin-bottom: 15px; border-bottom: 2px solid #eee; padding-bottom: 10px; }
|
.card h3 { color: #667eea; margin-bottom: 15px; border-bottom: 2px solid #eee; padding-bottom: 10px; }
|
||||||
|
.grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(320px,1fr)); gap:16px; }
|
||||||
.concept-box { background: #f8f9fa; border-left: 4px solid #667eea; padding: 15px; margin: 10px 0; border-radius: 5px; }
|
.concept-box { background: #f8f9fa; border-left: 4px solid #667eea; padding: 15px; margin: 10px 0; border-radius: 5px; }
|
||||||
.concept-box h4 { color: #333; margin-bottom: 10px; }
|
.lab { background:#fff7e6; border-left:4px solid #fa8c16; padding:15px; border-radius:8px; margin-bottom:20px; }
|
||||||
|
|
||||||
.btn { padding: 10px 20px; background: #667eea; color: white; border: none; border-radius: 5px; cursor: pointer; margin: 5px; }
|
.btn { padding: 10px 20px; background: #667eea; color: white; border: none; border-radius: 5px; cursor: pointer; margin: 5px; }
|
||||||
.btn:hover { background: #5a6fd6; }
|
.btn:hover { background: #5a6fd6; }
|
||||||
.btn-secondary { background: #6c757d; }
|
.btn-secondary { background: #6c757d; }
|
||||||
|
.btn-purple { background:#8e44ad; }
|
||||||
.result { background: #1e1e1e; color: #d4d4d4; padding: 15px; border-radius: 5px; margin-top: 10px; font-family: monospace; font-size: 0.9em; overflow-x: auto; max-height: 400px; overflow-y: auto; }
|
.result { background: #1e1e1e; color: #d4d4d4; padding: 15px; border-radius: 5px; margin-top: 10px; font-family: monospace; font-size: 0.9em; overflow-x: auto; max-height: 420px; overflow-y: auto; white-space: pre-wrap; }
|
||||||
|
|
||||||
table { width: 100%; border-collapse: collapse; margin: 10px 0; }
|
table { width: 100%; border-collapse: collapse; margin: 10px 0; }
|
||||||
th, td { padding: 10px; text-align: left; border-bottom: 1px solid #eee; }
|
th, td { padding: 10px; text-align: left; border-bottom: 1px solid #eee; }
|
||||||
th { background: #f8f9fa; font-weight: 600; }
|
th { background: #f8f9fa; font-weight: 600; }
|
||||||
tr:hover { background: #f8f9fa; }
|
tr:hover { background: #f8f9fa; }
|
||||||
|
|
||||||
.badge { padding: 4px 8px; border-radius: 4px; font-size: 0.8em; }
|
.badge { padding: 4px 8px; border-radius: 4px; font-size: 0.8em; }
|
||||||
.badge-primary { background: #667eea; color: white; }
|
.badge-primary { background: #667eea; color: white; }
|
||||||
.badge-success { background: #28a745; color: white; }
|
.badge-success { background: #28a745; color: white; }
|
||||||
.badge-warning { background: #ffc107; color: #333; }
|
.badge-warning { background: #ffc107; color: #333; }
|
||||||
|
.timeline { background:#0f172a; color:#e2e8f0; padding:14px; border-radius:8px; max-height:420px; overflow:auto; }
|
||||||
|
.timeline-item { padding:10px; border-left:3px solid #60a5fa; margin:10px 0; background:rgba(255,255,255,0.04); }
|
||||||
|
.small { color:#666; font-size:0.9em; line-height:1.7; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<h1>📦 IoC 容器学习</h1>
|
<h1>📦 IoC 生命周期可视化实验室</h1>
|
||||||
<p>控制反转 (Inversion of Control) 与依赖注入 (Dependency Injection)</p>
|
<p>把“类加载”“Bean 创建”“单例/多例/懒加载”拆开看,自己动手触发、自己观察结果。</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="nav">
|
<div class="nav">
|
||||||
<a href="index.html">🏠 首页</a>
|
<a href="index.html">🏠 首页</a>
|
||||||
<a href="ioc.html" class="active">📦 IoC</a>
|
<a href="ioc.html" class="active">📦 IoC</a>
|
||||||
|
<a href="verify-lab.html">🩺 修复验证</a>
|
||||||
<a href="aop.html">🔪 AOP</a>
|
<a href="aop.html">🔪 AOP</a>
|
||||||
<a href="mybatis.html">💾 MyBatis</a>
|
<a href="mybatis.html">💾 MyBatis</a>
|
||||||
<a href="transaction.html">🔄 事务</a>
|
<a href="transaction.html">🔄 事务</a>
|
||||||
<a href="users.html">👥 用户</a>
|
|
||||||
<a href="api.html">🔌 API</a>
|
<a href="api.html">🔌 API</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="lab">
|
||||||
<h3>📚 核心概念</h3>
|
<strong>🧪 实验任务卡</strong>
|
||||||
<div class="concept-box">
|
<ul style="padding-left:20px; line-height:1.8; margin-top:8px;">
|
||||||
<h4>什么是 IoC?</h4>
|
<li>先点“加载总览”,搞清楚 JVM 类加载 和 Spring Bean 生命周期不是一回事</li>
|
||||||
<p><strong>控制反转 (Inversion of Control)</strong>:将对象的创建和管理交给 Spring 容器,而不是由开发者手动创建。</p>
|
<li>再点“比较三种作用域”,对比 singleton / prototype / lazy singleton 的实例 ID 和 hashCode</li>
|
||||||
<p><strong>依赖注入 (Dependency Injection)</strong>:IoC 的一种实现方式,通过构造器、Setter 或字段将依赖注入到对象中。</p>
|
<li>然后分别连续点击“获取单例 / 获取多例 / 获取懒加载单例”,观察时间线变化</li>
|
||||||
|
<li>最后重置时间线,再重新做一遍,看看第一次触发和第二次触发的区别</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid">
|
||||||
|
<div class="card">
|
||||||
|
<h3>📚 核心概念总览</h3>
|
||||||
|
<button class="btn" onclick="loadOverview()">加载总览</button>
|
||||||
|
<div id="overviewResult" class="result">点击按钮查看“类加载 vs Bean 生命周期”...</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="concept-box">
|
|
||||||
<h4>为什么用 IoC?</h4>
|
<div class="card">
|
||||||
<ul>
|
<h3>🆚 作用域对比实验</h3>
|
||||||
<li>✅ <strong>解耦</strong>:对象之间不直接依赖,通过接口交互</li>
|
<button class="btn" onclick="compareScopes()">比较三种作用域</button>
|
||||||
<li>✅ <strong>可测试</strong>:方便使用 Mock 对象进行单元测试</li>
|
<button class="btn btn-secondary" onclick="resetTimeline()">重置时间线</button>
|
||||||
<li>✅ <strong>可维护</strong>:集中管理对象生命周期</li>
|
<div class="small">观察重点:singleton 两次获取是否同一实例?prototype 为什么每次都变?lazy singleton 第一次获取前是否就已经创建?</div>
|
||||||
<li>✅ <strong>AOP 支持</strong>:便于实现切面编程</li>
|
<div id="compareResult" class="result"></div>
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>🎯 单点触发实验</h3>
|
||||||
|
<button class="btn" onclick="inspect('singleton')">获取单例 Bean</button>
|
||||||
|
<button class="btn btn-purple" onclick="inspect('prototype')">获取多例 Bean</button>
|
||||||
|
<button class="btn btn-secondary" onclick="inspect('lazy')">获取懒加载单例</button>
|
||||||
|
<div id="inspectResult" class="result">点击上面的按钮,观察实例 ID / hashCode / 访问次数如何变化...</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid">
|
||||||
|
<div class="card">
|
||||||
|
<h3>🕰️ 生命周期时间线</h3>
|
||||||
|
<button class="btn" onclick="loadTimeline()">刷新时间线</button>
|
||||||
|
<div id="timelineResult" class="timeline">时间线加载中...</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>📊 Bean 作用域解释</h3>
|
||||||
|
<table>
|
||||||
|
<tr><th>作用域</th><th>创建时机</th><th>实例特点</th></tr>
|
||||||
|
<tr><td><span class="badge badge-primary">singleton</span></td><td>通常容器启动时</td><td>全局一个实例,反复获取同一个对象</td></tr>
|
||||||
|
<tr><td><span class="badge badge-success">prototype</span></td><td>每次 getBean 时</td><td>每次都是新实例,不进单例池</td></tr>
|
||||||
|
<tr><td><span class="badge badge-warning">lazy singleton</span></td><td>第一次真正使用时</td><td>启动不创建,首次获取才创建,之后复用</td></tr>
|
||||||
|
</table>
|
||||||
|
<div class="concept-box">
|
||||||
|
<h4>记住这句话</h4>
|
||||||
|
<p><strong>类加载 ≠ Bean 创建;Bean 创建 ≠ 每次请求都 new。</strong></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h3>🔍 查看所有 Bean</h3>
|
<h3>🔍 查看所有 Bean</h3>
|
||||||
<p>Spring 容器中管理的所有 Bean 对象</p>
|
|
||||||
<button class="btn" onclick="loadBeans()">刷新 Bean 列表</button>
|
<button class="btn" onclick="loadBeans()">刷新 Bean 列表</button>
|
||||||
<button class="btn btn-secondary" onclick="document.getElementById('beansResult').innerHTML=''">清空</button>
|
<button class="btn btn-secondary" onclick="document.getElementById('beansResult').innerHTML=''">清空</button>
|
||||||
<div id="beansResult" class="result"></div>
|
<div id="beansResult" class="result"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<h3>📊 Bean 作用域</h3>
|
|
||||||
<table>
|
|
||||||
<tr><th>作用域</th><th>说明</th><th>使用场景</th></tr>
|
|
||||||
<tr><td><span class="badge badge-primary">singleton</span></td><td>默认,整个应用只有一个实例</td><td>无状态的服务、配置类</td></tr>
|
|
||||||
<tr><td><span class="badge badge-success">prototype</span></td><td>每次请求都创建新实例</td><td>有状态的对象</td></tr>
|
|
||||||
<tr><td><span class="badge badge-warning">request</span></td><td>每个 HTTP 请求一个实例</td><td>Web 应用</td></tr>
|
|
||||||
<tr><td><span class="badge badge-warning">session</span></td><td>每个 HTTP 会话一个实例</td><td>用户会话数据</td></tr>
|
|
||||||
</table>
|
|
||||||
<button class="btn" onclick="testScopes()">测试作用域</button>
|
|
||||||
<div id="scopesResult" class="result"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<h3>⚡ 性能统计</h3>
|
|
||||||
<p>实时监控方法执行时间和调用次数</p>
|
|
||||||
<button class="btn" onclick="loadPerformance()">刷新统计</button>
|
|
||||||
<button class="btn btn-secondary" onclick="resetPerformance()">重置统计</button>
|
|
||||||
<div id="performanceResult" class="result"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<h3>💉 依赖注入方式对比</h3>
|
|
||||||
<table>
|
|
||||||
<tr><th>方式</th><th>优点</th><th>缺点</th><th>推荐度</th></tr>
|
|
||||||
<tr><td>构造器注入</td><td>明确依赖、不可变、易测试</td><td>参数多时代码长</td><td>⭐⭐⭐⭐⭐</td></tr>
|
|
||||||
<tr><td>Setter 注入</td><td>可选依赖、灵活</td><td>可能为 null</td><td>⭐⭐⭐</td></tr>
|
|
||||||
<tr><td>字段注入</td><td>代码简洁</td><td>隐藏依赖、难测试</td><td>⭐</td></tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
async function loadOverview() {
|
||||||
|
const result = document.getElementById('overviewResult');
|
||||||
|
result.textContent = '加载中...';
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/learning/ioc/lifecycle/overview');
|
||||||
|
const data = await res.json();
|
||||||
|
result.textContent = JSON.stringify(data, null, 2);
|
||||||
|
} catch (e) {
|
||||||
|
result.textContent = '加载失败: ' + e.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function inspect(scope) {
|
||||||
|
const result = document.getElementById('inspectResult');
|
||||||
|
result.textContent = '触发中...';
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/learning/ioc/lifecycle/inspect/${scope}?trigger=ui-click`);
|
||||||
|
const data = await res.json();
|
||||||
|
result.textContent = JSON.stringify(data, null, 2);
|
||||||
|
loadTimeline();
|
||||||
|
} catch (e) {
|
||||||
|
result.textContent = '触发失败: ' + e.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function compareScopes() {
|
||||||
|
const result = document.getElementById('compareResult');
|
||||||
|
result.textContent = '对比中...';
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/learning/ioc/lifecycle/compare');
|
||||||
|
const data = await res.json();
|
||||||
|
result.textContent = JSON.stringify(data, null, 2);
|
||||||
|
loadTimeline();
|
||||||
|
} catch (e) {
|
||||||
|
result.textContent = '对比失败: ' + e.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadTimeline() {
|
||||||
|
const result = document.getElementById('timelineResult');
|
||||||
|
result.textContent = '加载中...';
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/learning/ioc/lifecycle/timeline');
|
||||||
|
const data = await res.json();
|
||||||
|
if (!data.length) {
|
||||||
|
result.textContent = '暂无时间线事件,先点上面的实验按钮。';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
result.innerHTML = data.map(item => `
|
||||||
|
<div class="timeline-item">
|
||||||
|
<div><strong>#${item.seq}</strong> [${item.scope}] ${item.beanName}</div>
|
||||||
|
<div>${item.phase}</div>
|
||||||
|
<div style="color:#94a3b8; margin-top:6px;">${item.detail}</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
} catch (e) {
|
||||||
|
result.textContent = '加载失败: ' + e.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resetTimeline() {
|
||||||
|
await fetch('/api/learning/ioc/lifecycle/reset', { method: 'POST' });
|
||||||
|
loadTimeline();
|
||||||
|
}
|
||||||
|
|
||||||
async function loadBeans() {
|
async function loadBeans() {
|
||||||
const result = document.getElementById('beansResult');
|
const result = document.getElementById('beansResult');
|
||||||
result.textContent = '加载中...';
|
result.textContent = '加载中...';
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/learning/ioc/beans');
|
const res = await fetch('/api/learning/ioc/beans');
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
result.innerHTML = `<strong>总 Bean 数: ${data.total}</strong>\n\n用户相关 Bean:\n${data.userBeans.map(b => ' - ' + b).join('\n')}`;
|
result.innerHTML = `<strong>总 Bean 数: ${data.total}</strong>\n\nIoC 学习相关 Bean:\n${data.userBeans.map(b => ' - ' + b).join('\n')}`;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
result.textContent = '加载失败: ' + e.message;
|
result.textContent = '加载失败: ' + e.message;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function testScopes() {
|
loadOverview();
|
||||||
const result = document.getElementById('scopesResult');
|
loadTimeline();
|
||||||
try {
|
|
||||||
const res = await fetch('/api/learning/ioc/scopes');
|
|
||||||
const data = await res.json();
|
|
||||||
result.innerHTML = JSON.stringify(data, null, 2);
|
|
||||||
} catch (e) {
|
|
||||||
result.textContent = '测试失败: ' + e.message;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadPerformance() {
|
|
||||||
const result = document.getElementById('performanceResult');
|
|
||||||
result.textContent = '加载中...';
|
|
||||||
try {
|
|
||||||
const res = await fetch('/api/learning/ioc/performance');
|
|
||||||
const data = await res.json();
|
|
||||||
if (Object.keys(data).length === 0) {
|
|
||||||
result.textContent = '暂无性能数据,请先调用一些 API';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let html = '<table><tr><th>方法</th><th>调用次数</th><th>错误数</th><th>平均耗时(ms)</th><th>最大耗时(ms)</th></tr>';
|
|
||||||
for (const [key, val] of Object.entries(data)) {
|
|
||||||
html += `<tr><td>${key}</td><td>${val.count}</td><td>${val.errors}</td><td>${val.avgMs}</td><td>${val.maxMs}</td></tr>`;
|
|
||||||
}
|
|
||||||
html += '</table>';
|
|
||||||
result.innerHTML = html;
|
|
||||||
} catch (e) {
|
|
||||||
result.textContent = '加载失败: ' + e.message;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function resetPerformance() {
|
|
||||||
try {
|
|
||||||
await fetch('/api/learning/ioc/performance/reset', { method: 'POST' });
|
|
||||||
loadPerformance();
|
|
||||||
} catch (e) {
|
|
||||||
alert('重置失败: ' + e.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
loadBeans();
|
|
||||||
loadPerformance();
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user