feat: Spring Boot 学习脚手架 v2.0

- 新增 IoC 容器学习模块
- 新增 AOP 切面编程学习模块
- 新增 MyBatis 集成学习模块
- 新增事务管理学习模块
- 新增用户/产品/订单 CRUD
- 新增 7 个交互式学习页面
- 集成性能监控切面
This commit is contained in:
likingcode
2026-03-07 08:37:40 +00:00
commit c04235c655
73 changed files with 4978 additions and 0 deletions

92
pom.xml Normal file
View File

@@ -0,0 +1,92 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.3</version>
</parent>
<groupId>com.example</groupId>
<artifactId>springboot-scaffold</artifactId>
<version>1.0.0</version>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot AOP -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- Spring Boot Validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- MyBatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
<!-- Spring Data JPA (对比学习) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- H2 Database -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Lombok (简化代码) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Spring Boot Actuator (监控) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>

193
springboot.log Normal file
View File

@@ -0,0 +1,193 @@
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.2.3)
2026-03-07T08:34:17.523Z INFO 1388476 --- [springboot-scaffold] [ main] c.e.s.SpringbootScaffoldApplication : Starting SpringbootScaffoldApplication v1.0.0 using Java 21.0.10 with PID 1388476 (/home/llm/Projects/springboot-scaffold/target/springboot-scaffold-1.0.0.jar started by llm in /home/llm/Projects/springboot-scaffold)
2026-03-07T08:34:17.535Z DEBUG 1388476 --- [springboot-scaffold] [ main] c.e.s.SpringbootScaffoldApplication : Running with Spring Boot v3.2.3, Spring v6.1.4
2026-03-07T08:34:17.539Z INFO 1388476 --- [springboot-scaffold] [ main] c.e.s.SpringbootScaffoldApplication : No active profile set, falling back to 1 default profile: "default"
2026-03-07T08:34:23.786Z INFO 1388476 --- [springboot-scaffold] [ main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode.
2026-03-07T08:34:23.807Z INFO 1388476 --- [springboot-scaffold] [ main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 11 ms. Found 0 JPA repository interfaces.
2026-03-07T08:34:30.399Z INFO 1388476 --- [springboot-scaffold] [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port 8082 (http)
2026-03-07T08:34:30.466Z INFO 1388476 --- [springboot-scaffold] [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2026-03-07T08:34:30.467Z INFO 1388476 --- [springboot-scaffold] [ main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/10.1.19]
2026-03-07T08:34:31.033Z INFO 1388476 --- [springboot-scaffold] [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2026-03-07T08:34:31.043Z INFO 1388476 --- [springboot-scaffold] [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 13168 ms
2026-03-07T08:34:32.832Z INFO 1388476 --- [springboot-scaffold] [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
2026-03-07T08:34:34.286Z INFO 1388476 --- [springboot-scaffold] [ main] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Added connection conn0: url=jdbc:h2:file:~/h2/springboot_scaffold user=SA
2026-03-07T08:34:34.295Z INFO 1388476 --- [springboot-scaffold] [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
2026-03-07T08:34:34.393Z INFO 1388476 --- [springboot-scaffold] [ main] o.s.b.a.h2.H2ConsoleAutoConfiguration : H2 console available at '/h2-console'. Database available at 'jdbc:h2:file:~/h2/springboot_scaffold'
2026-03-07T08:34:37.699Z INFO 1388476 --- [springboot-scaffold] [ main] o.hibernate.jpa.internal.util.LogHelper : HHH000204: Processing PersistenceUnitInfo [name: default]
2026-03-07T08:34:38.061Z INFO 1388476 --- [springboot-scaffold] [ main] org.hibernate.Version : HHH000412: Hibernate ORM core version 6.4.4.Final
2026-03-07T08:34:38.254Z INFO 1388476 --- [springboot-scaffold] [ main] o.h.c.internal.RegionFactoryInitiator : HHH000026: Second-level cache disabled
2026-03-07T08:34:39.633Z INFO 1388476 --- [springboot-scaffold] [ main] o.s.o.j.p.SpringPersistenceUnitInfo : No LoadTimeWeaver setup: ignoring JPA class transformer
2026-03-07T08:34:45.616Z INFO 1388476 --- [springboot-scaffold] [ main] o.h.e.t.j.p.i.JtaPlatformInitiator : HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration)
Hibernate:
create table orders (
id bigint generated by default as identity,
created_at timestamp(6),
product_id bigint not null,
quantity integer not null,
status varchar(20),
total_price numeric(10,2) not null,
user_id bigint not null,
primary key (id)
)
Hibernate:
create table products (
id bigint generated by default as identity,
category varchar(50),
created_at timestamp(6),
description varchar(500),
name varchar(100) not null,
price numeric(10,2) not null,
stock_quantity integer,
primary key (id)
)
Hibernate:
create table users (
id bigint generated by default as identity,
active boolean not null,
bio TEXT,
created_at timestamp(6),
email varchar(100) not null,
phone varchar(20),
updated_at timestamp(6),
username varchar(50) not null,
primary key (id)
)
2026-03-07T08:34:45.879Z INFO 1388476 --- [springboot-scaffold] [ main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
Logging initialized using 'class org.apache.ibatis.logging.stdout.StdOutImpl' adapter.
2026-03-07T08:34:47.256Z ERROR 1388476 --- [springboot-scaffold] [ main] o.m.spring.mapper.MapperFactoryBean : Error while adding the mapper 'interface com.example.scaffold.mapper.UserMapper' to configuration.
org.apache.ibatis.cache.CacheException: Invalid base cache implementation (class org.apache.ibatis.cache.decorators.LruCache). Base cache implementations must have a constructor that takes a String id as a parameter. Cause: java.lang.NoSuchMethodException: org.apache.ibatis.cache.decorators.LruCache.<init>(java.lang.String)
at org.apache.ibatis.mapping.CacheBuilder.getBaseCacheConstructor(CacheBuilder.java:195) ~[mybatis-3.5.14.jar!/:3.5.14]
at org.apache.ibatis.mapping.CacheBuilder.newBaseCacheInstance(CacheBuilder.java:183) ~[mybatis-3.5.14.jar!/:3.5.14]
at org.apache.ibatis.mapping.CacheBuilder.build(CacheBuilder.java:94) ~[mybatis-3.5.14.jar!/:3.5.14]
at org.apache.ibatis.builder.MapperBuilderAssistant.useNewCache(MapperBuilderAssistant.java:128) ~[mybatis-3.5.14.jar!/:3.5.14]
at org.apache.ibatis.builder.annotation.MapperAnnotationBuilder.parseCache(MapperAnnotationBuilder.java:191) ~[mybatis-3.5.14.jar!/:3.5.14]
at org.apache.ibatis.builder.annotation.MapperAnnotationBuilder.parse(MapperAnnotationBuilder.java:121) ~[mybatis-3.5.14.jar!/:3.5.14]
at org.apache.ibatis.binding.MapperRegistry.addMapper(MapperRegistry.java:72) ~[mybatis-3.5.14.jar!/:3.5.14]
at org.apache.ibatis.session.Configuration.addMapper(Configuration.java:895) ~[mybatis-3.5.14.jar!/:3.5.14]
at org.mybatis.spring.mapper.MapperFactoryBean.checkDaoConfig(MapperFactoryBean.java:80) ~[mybatis-spring-3.0.3.jar!/:3.0.3]
at org.springframework.dao.support.DaoSupport.afterPropertiesSet(DaoSupport.java:44) ~[spring-tx-6.1.4.jar!/:6.1.4]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1833) ~[spring-beans-6.1.4.jar!/:6.1.4]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1782) ~[spring-beans-6.1.4.jar!/:6.1.4]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:600) ~[spring-beans-6.1.4.jar!/:6.1.4]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:522) ~[spring-beans-6.1.4.jar!/:6.1.4]
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:325) ~[spring-beans-6.1.4.jar!/:6.1.4]
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-6.1.4.jar!/:6.1.4]
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:323) ~[spring-beans-6.1.4.jar!/:6.1.4]
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199) ~[spring-beans-6.1.4.jar!/:6.1.4]
at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:254) ~[spring-beans-6.1.4.jar!/:6.1.4]
at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1443) ~[spring-beans-6.1.4.jar!/:6.1.4]
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1353) ~[spring-beans-6.1.4.jar!/:6.1.4]
at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:907) ~[spring-beans-6.1.4.jar!/:6.1.4]
at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:785) ~[spring-beans-6.1.4.jar!/:6.1.4]
at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:237) ~[spring-beans-6.1.4.jar!/:6.1.4]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1355) ~[spring-beans-6.1.4.jar!/:6.1.4]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1192) ~[spring-beans-6.1.4.jar!/:6.1.4]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:562) ~[spring-beans-6.1.4.jar!/:6.1.4]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:522) ~[spring-beans-6.1.4.jar!/:6.1.4]
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:325) ~[spring-beans-6.1.4.jar!/:6.1.4]
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-6.1.4.jar!/:6.1.4]
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:323) ~[spring-beans-6.1.4.jar!/:6.1.4]
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199) ~[spring-beans-6.1.4.jar!/:6.1.4]
at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:975) ~[spring-beans-6.1.4.jar!/:6.1.4]
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:959) ~[spring-context-6.1.4.jar!/:6.1.4]
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:624) ~[spring-context-6.1.4.jar!/:6.1.4]
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:146) ~[spring-boot-3.2.3.jar!/:3.2.3]
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:754) ~[spring-boot-3.2.3.jar!/:3.2.3]
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:456) ~[spring-boot-3.2.3.jar!/:3.2.3]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:334) ~[spring-boot-3.2.3.jar!/:3.2.3]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1354) ~[spring-boot-3.2.3.jar!/:3.2.3]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1343) ~[spring-boot-3.2.3.jar!/:3.2.3]
at com.example.scaffold.SpringbootScaffoldApplication.main(SpringbootScaffoldApplication.java:9) ~[!/:1.0.0]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) ~[na:na]
at java.base/java.lang.reflect.Method.invoke(Method.java:580) ~[na:na]
at org.springframework.boot.loader.launch.Launcher.launch(Launcher.java:91) ~[springboot-scaffold-1.0.0.jar:1.0.0]
at org.springframework.boot.loader.launch.Launcher.launch(Launcher.java:53) ~[springboot-scaffold-1.0.0.jar:1.0.0]
at org.springframework.boot.loader.launch.JarLauncher.main(JarLauncher.java:58) ~[springboot-scaffold-1.0.0.jar:1.0.0]
Caused by: java.lang.NoSuchMethodException: org.apache.ibatis.cache.decorators.LruCache.<init>(java.lang.String)
at java.base/java.lang.Class.getConstructor0(Class.java:3763) ~[na:na]
at java.base/java.lang.Class.getConstructor(Class.java:2444) ~[na:na]
at org.apache.ibatis.mapping.CacheBuilder.getBaseCacheConstructor(CacheBuilder.java:193) ~[mybatis-3.5.14.jar!/:3.5.14]
... 46 common frames omitted
2026-03-07T08:34:47.276Z WARN 1388476 --- [springboot-scaffold] [ main] ConfigServletWebServerApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'orderService' defined in URL [jar:nested:/home/llm/Projects/springboot-scaffold/target/springboot-scaffold-1.0.0.jar/!BOOT-INF/classes/!/com/example/scaffold/service/impl/OrderService.class]: Unsatisfied dependency expressed through constructor parameter 2: Error creating bean with name 'userMapper' defined in URL [jar:nested:/home/llm/Projects/springboot-scaffold/target/springboot-scaffold-1.0.0.jar/!BOOT-INF/classes/!/com/example/scaffold/mapper/UserMapper.class]: org.apache.ibatis.cache.CacheException: Invalid base cache implementation (class org.apache.ibatis.cache.decorators.LruCache). Base cache implementations must have a constructor that takes a String id as a parameter. Cause: java.lang.NoSuchMethodException: org.apache.ibatis.cache.decorators.LruCache.<init>(java.lang.String)
2026-03-07T08:34:47.280Z INFO 1388476 --- [springboot-scaffold] [ main] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
2026-03-07T08:34:47.299Z INFO 1388476 --- [springboot-scaffold] [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown initiated...
2026-03-07T08:34:47.328Z INFO 1388476 --- [springboot-scaffold] [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown completed.
2026-03-07T08:34:47.337Z INFO 1388476 --- [springboot-scaffold] [ main] o.apache.catalina.core.StandardService : Stopping service [Tomcat]
2026-03-07T08:34:47.461Z INFO 1388476 --- [springboot-scaffold] [ main] .s.b.a.l.ConditionEvaluationReportLogger :
Error starting ApplicationContext. To display the condition evaluation report re-run your application with 'debug' enabled.
2026-03-07T08:34:47.549Z ERROR 1388476 --- [springboot-scaffold] [ main] o.s.boot.SpringApplication : Application run failed
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'orderService' defined in URL [jar:nested:/home/llm/Projects/springboot-scaffold/target/springboot-scaffold-1.0.0.jar/!BOOT-INF/classes/!/com/example/scaffold/service/impl/OrderService.class]: Unsatisfied dependency expressed through constructor parameter 2: Error creating bean with name 'userMapper' defined in URL [jar:nested:/home/llm/Projects/springboot-scaffold/target/springboot-scaffold-1.0.0.jar/!BOOT-INF/classes/!/com/example/scaffold/mapper/UserMapper.class]: org.apache.ibatis.cache.CacheException: Invalid base cache implementation (class org.apache.ibatis.cache.decorators.LruCache). Base cache implementations must have a constructor that takes a String id as a parameter. Cause: java.lang.NoSuchMethodException: org.apache.ibatis.cache.decorators.LruCache.<init>(java.lang.String)
at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:798) ~[spring-beans-6.1.4.jar!/:6.1.4]
at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:237) ~[spring-beans-6.1.4.jar!/:6.1.4]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1355) ~[spring-beans-6.1.4.jar!/:6.1.4]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1192) ~[spring-beans-6.1.4.jar!/:6.1.4]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:562) ~[spring-beans-6.1.4.jar!/:6.1.4]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:522) ~[spring-beans-6.1.4.jar!/:6.1.4]
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:325) ~[spring-beans-6.1.4.jar!/:6.1.4]
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-6.1.4.jar!/:6.1.4]
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:323) ~[spring-beans-6.1.4.jar!/:6.1.4]
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199) ~[spring-beans-6.1.4.jar!/:6.1.4]
at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:975) ~[spring-beans-6.1.4.jar!/:6.1.4]
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:959) ~[spring-context-6.1.4.jar!/:6.1.4]
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:624) ~[spring-context-6.1.4.jar!/:6.1.4]
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:146) ~[spring-boot-3.2.3.jar!/:3.2.3]
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:754) ~[spring-boot-3.2.3.jar!/:3.2.3]
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:456) ~[spring-boot-3.2.3.jar!/:3.2.3]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:334) ~[spring-boot-3.2.3.jar!/:3.2.3]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1354) ~[spring-boot-3.2.3.jar!/:3.2.3]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1343) ~[spring-boot-3.2.3.jar!/:3.2.3]
at com.example.scaffold.SpringbootScaffoldApplication.main(SpringbootScaffoldApplication.java:9) ~[!/:1.0.0]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) ~[na:na]
at java.base/java.lang.reflect.Method.invoke(Method.java:580) ~[na:na]
at org.springframework.boot.loader.launch.Launcher.launch(Launcher.java:91) ~[springboot-scaffold-1.0.0.jar:1.0.0]
at org.springframework.boot.loader.launch.Launcher.launch(Launcher.java:53) ~[springboot-scaffold-1.0.0.jar:1.0.0]
at org.springframework.boot.loader.launch.JarLauncher.main(JarLauncher.java:58) ~[springboot-scaffold-1.0.0.jar:1.0.0]
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'userMapper' defined in URL [jar:nested:/home/llm/Projects/springboot-scaffold/target/springboot-scaffold-1.0.0.jar/!BOOT-INF/classes/!/com/example/scaffold/mapper/UserMapper.class]: org.apache.ibatis.cache.CacheException: Invalid base cache implementation (class org.apache.ibatis.cache.decorators.LruCache). Base cache implementations must have a constructor that takes a String id as a parameter. Cause: java.lang.NoSuchMethodException: org.apache.ibatis.cache.decorators.LruCache.<init>(java.lang.String)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1786) ~[spring-beans-6.1.4.jar!/:6.1.4]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:600) ~[spring-beans-6.1.4.jar!/:6.1.4]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:522) ~[spring-beans-6.1.4.jar!/:6.1.4]
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:325) ~[spring-beans-6.1.4.jar!/:6.1.4]
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-6.1.4.jar!/:6.1.4]
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:323) ~[spring-beans-6.1.4.jar!/:6.1.4]
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199) ~[spring-beans-6.1.4.jar!/:6.1.4]
at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:254) ~[spring-beans-6.1.4.jar!/:6.1.4]
at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1443) ~[spring-beans-6.1.4.jar!/:6.1.4]
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1353) ~[spring-beans-6.1.4.jar!/:6.1.4]
at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:907) ~[spring-beans-6.1.4.jar!/:6.1.4]
at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:785) ~[spring-beans-6.1.4.jar!/:6.1.4]
... 24 common frames omitted
Caused by: java.lang.IllegalArgumentException: org.apache.ibatis.cache.CacheException: Invalid base cache implementation (class org.apache.ibatis.cache.decorators.LruCache). Base cache implementations must have a constructor that takes a String id as a parameter. Cause: java.lang.NoSuchMethodException: org.apache.ibatis.cache.decorators.LruCache.<init>(java.lang.String)
at org.mybatis.spring.mapper.MapperFactoryBean.checkDaoConfig(MapperFactoryBean.java:83) ~[mybatis-spring-3.0.3.jar!/:3.0.3]
at org.springframework.dao.support.DaoSupport.afterPropertiesSet(DaoSupport.java:44) ~[spring-tx-6.1.4.jar!/:6.1.4]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1833) ~[spring-beans-6.1.4.jar!/:6.1.4]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1782) ~[spring-beans-6.1.4.jar!/:6.1.4]
... 35 common frames omitted
Caused by: org.apache.ibatis.cache.CacheException: Invalid base cache implementation (class org.apache.ibatis.cache.decorators.LruCache). Base cache implementations must have a constructor that takes a String id as a parameter. Cause: java.lang.NoSuchMethodException: org.apache.ibatis.cache.decorators.LruCache.<init>(java.lang.String)
at org.apache.ibatis.mapping.CacheBuilder.getBaseCacheConstructor(CacheBuilder.java:195) ~[mybatis-3.5.14.jar!/:3.5.14]
at org.apache.ibatis.mapping.CacheBuilder.newBaseCacheInstance(CacheBuilder.java:183) ~[mybatis-3.5.14.jar!/:3.5.14]
at org.apache.ibatis.mapping.CacheBuilder.build(CacheBuilder.java:94) ~[mybatis-3.5.14.jar!/:3.5.14]
at org.apache.ibatis.builder.MapperBuilderAssistant.useNewCache(MapperBuilderAssistant.java:128) ~[mybatis-3.5.14.jar!/:3.5.14]
at org.apache.ibatis.builder.annotation.MapperAnnotationBuilder.parseCache(MapperAnnotationBuilder.java:191) ~[mybatis-3.5.14.jar!/:3.5.14]
at org.apache.ibatis.builder.annotation.MapperAnnotationBuilder.parse(MapperAnnotationBuilder.java:121) ~[mybatis-3.5.14.jar!/:3.5.14]
at org.apache.ibatis.binding.MapperRegistry.addMapper(MapperRegistry.java:72) ~[mybatis-3.5.14.jar!/:3.5.14]
at org.apache.ibatis.session.Configuration.addMapper(Configuration.java:895) ~[mybatis-3.5.14.jar!/:3.5.14]
at org.mybatis.spring.mapper.MapperFactoryBean.checkDaoConfig(MapperFactoryBean.java:80) ~[mybatis-spring-3.0.3.jar!/:3.0.3]
... 38 common frames omitted
Caused by: java.lang.NoSuchMethodException: org.apache.ibatis.cache.decorators.LruCache.<init>(java.lang.String)
at java.base/java.lang.Class.getConstructor0(Class.java:3763) ~[na:na]
at java.base/java.lang.Class.getConstructor(Class.java:2444) ~[na:na]
at org.apache.ibatis.mapping.CacheBuilder.getBaseCacheConstructor(CacheBuilder.java:193) ~[mybatis-3.5.14.jar!/:3.5.14]
... 46 common frames omitted

View File

@@ -0,0 +1,11 @@
package com.example.scaffold;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SpringbootScaffoldApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootScaffoldApplication.class, args);
}
}

View File

@@ -0,0 +1,115 @@
package com.example.scaffold.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import java.util.Arrays;
/**
* 学习切面 - 演示 AOP 各种通知类型
*
* 学习要点:
* 1. @Aspect - 标记为切面类
* 2. @Pointcut - 切入点表达式
* 3. @Before - 前置通知
* 4. @After - 后置通知
* 5. @AfterReturning - 返回通知
* 6. @AfterThrowing - 异常通知
* 7. @Around - 环绕通知
*/
@Slf4j
@Aspect
@Component
public class LearningAspect {
/**
* 切入点 - 所有 Service 层方法
*/
@Pointcut("execution(* com.example.scaffold.service..*.*(..))")
public void serviceLayer() {}
/**
* 切入点 - 所有 Controller 层方法
*/
@Pointcut("execution(* com.example.scaffold.controller..*.*(..))")
public void controllerLayer() {}
/**
* 切入点 - 所有 Mapper 方法
*/
@Pointcut("execution(* com.example.scaffold.mapper..*.*(..))")
public void mapperLayer() {}
/**
* 前置通知 - 方法执行前
*/
@Before("serviceLayer()")
public void beforeService(JoinPoint joinPoint) {
log.info("🔹 [AOP @Before] 即将执行: {}.{}()",
joinPoint.getTarget().getClass().getSimpleName(),
joinPoint.getSignature().getName());
}
/**
* 后置通知 - 方法执行后(无论是否异常)
*/
@After("serviceLayer()")
public void afterService(JoinPoint joinPoint) {
log.info("🔸 [AOP @After] 执行完成: {}.{}()",
joinPoint.getTarget().getClass().getSimpleName(),
joinPoint.getSignature().getName());
}
/**
* 返回通知 - 方法成功返回后
*/
@AfterReturning(pointcut = "serviceLayer()", returning = "result")
public void afterReturningService(JoinPoint joinPoint, Object result) {
log.info("✅ [AOP @AfterReturning] 方法返回: {}.{}() => {}",
joinPoint.getTarget().getClass().getSimpleName(),
joinPoint.getSignature().getName(),
result != null ? result.getClass().getSimpleName() : "null");
}
/**
* 异常通知 - 方法抛出异常后
*/
@AfterThrowing(pointcut = "serviceLayer()", throwing = "ex")
public void afterThrowingService(JoinPoint joinPoint, Throwable ex) {
log.error("❌ [AOP @AfterThrowing] 方法异常: {}.{}() => {}",
joinPoint.getTarget().getClass().getSimpleName(),
joinPoint.getSignature().getName(),
ex.getMessage());
}
/**
* 环绕通知 - 完全控制方法执行
*/
@Around("controllerLayer()")
public Object aroundController(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
String className = joinPoint.getTarget().getClass().getSimpleName();
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
log.info("🔄 [AOP @Around] 开始: {}.{}() 参数: {}", className, methodName, Arrays.toString(args));
try {
// 执行目标方法
Object result = joinPoint.proceed();
long duration = System.currentTimeMillis() - startTime;
log.info("🔄 [AOP @Around] 完成: {}.{}() 耗时: {}ms", className, methodName, duration);
return result;
} catch (Throwable ex) {
long duration = System.currentTimeMillis() - startTime;
log.error("🔄 [AOP @Around] 异常: {}.{}() 耗时: {}ms 错误: {}",
className, methodName, duration, ex.getMessage());
throw ex;
}
}
}

View File

@@ -0,0 +1,67 @@
package com.example.scaffold.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
/**
* 性能监控切面 - 统计方法执行时间
*/
@Slf4j
@Aspect
@Component
public class PerformanceAspect {
private final Map<String, MethodStats> statsMap = new ConcurrentHashMap<>();
public static class MethodStats {
public final AtomicLong totalCount = new AtomicLong();
public final AtomicLong totalTime = new AtomicLong();
public final AtomicLong maxTime = new AtomicLong();
public final AtomicLong errorCount = new AtomicLong();
}
@Around("execution(* com.example.scaffold..*.*(..))")
public Object monitorPerformance(ProceedingJoinPoint joinPoint) throws Throwable {
String key = joinPoint.getTarget().getClass().getSimpleName() + "." + joinPoint.getSignature().getName();
long startTime = System.nanoTime();
try {
Object result = joinPoint.proceed();
recordSuccess(key, System.nanoTime() - startTime);
return result;
} catch (Throwable ex) {
recordError(key, System.nanoTime() - startTime);
throw ex;
}
}
private void recordSuccess(String key, long durationNanos) {
MethodStats stats = statsMap.computeIfAbsent(key, k -> new MethodStats());
stats.totalCount.incrementAndGet();
stats.totalTime.addAndGet(durationNanos);
long durationMs = durationNanos / 1_000_000;
stats.maxTime.updateAndGet(current -> Math.max(current, durationMs));
}
private void recordError(String key, long durationNanos) {
MethodStats stats = statsMap.computeIfAbsent(key, k -> new MethodStats());
stats.totalCount.incrementAndGet();
stats.errorCount.incrementAndGet();
stats.totalTime.addAndGet(durationNanos);
}
public Map<String, MethodStats> getStats() {
return new ConcurrentHashMap<>(statsMap);
}
public void resetStats() {
statsMap.clear();
}
}

View File

@@ -0,0 +1,38 @@
package com.example.scaffold.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
/**
* 应用配置类
*
* 学习要点:
* 1. @Configuration - 标记为配置类
* 2. @Bean - 声明 Bean
* 3. @EnableJpaAuditing - 启用 JPA 审计
*/
@Configuration
@EnableJpaAuditing
public class AppConfig {
/**
* CORS 配置 - 允许跨域请求
*/
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOriginPattern("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
}

View File

@@ -0,0 +1,18 @@
package com.example.scaffold.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@GetMapping("/api/hello")
public String hello() {
return "Hello from Spring Boot Scaffold!";
}
@GetMapping("/api/health")
public String health() {
return "OK";
}
}

View File

@@ -0,0 +1,125 @@
package com.example.scaffold.controller;
import com.example.scaffold.dto.OrderCreateRequest;
import com.example.scaffold.dto.ProductCreateRequest;
import com.example.scaffold.entity.Order;
import com.example.scaffold.entity.Product;
import com.example.scaffold.mapper.ProductMapper;
import com.example.scaffold.service.impl.OrderService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
/**
* 产品和订单控制器 - 演示事务
*/
@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
public class ProductOrderController {
private final ProductMapper productMapper;
private final OrderService orderService;
// ==================== 产品 API ====================
@GetMapping("/products")
public List<Product> listProducts() {
return productMapper.findAll();
}
@GetMapping("/products/{id}")
public ResponseEntity<Product> getProduct(@PathVariable Long id) {
Product product = productMapper.findById(id);
return product != null ? ResponseEntity.ok(product) : ResponseEntity.notFound().build();
}
@GetMapping("/products/category/{category}")
public List<Product> getByCategory(@PathVariable String category) {
return productMapper.findByCategory(category);
}
@GetMapping("/products/price")
public List<Product> getByPriceRange(@RequestParam BigDecimal min, @RequestParam BigDecimal max) {
return productMapper.findByPriceRange(min, max);
}
@PostMapping("/products")
public ResponseEntity<Product> createProduct(@Valid @RequestBody ProductCreateRequest request) {
Product product = new Product();
product.setName(request.getName());
product.setDescription(request.getDescription());
product.setPrice(request.getPrice());
product.setStockQuantity(request.getStockQuantity());
product.setCategory(request.getCategory());
productMapper.insert(product);
return ResponseEntity.ok(product);
}
@DeleteMapping("/products/{id}")
public ResponseEntity<Void> deleteProduct(@PathVariable Long id) {
productMapper.deleteById(id);
return ResponseEntity.noContent().build();
}
// ==================== 订单 API ====================
@GetMapping("/orders")
public List<Order> listOrders() {
return orderService.findAll();
}
@GetMapping("/orders/{id}")
public ResponseEntity<Order> getOrder(@PathVariable Long id) {
Order order = orderService.getOrder(id);
return order != null ? ResponseEntity.ok(order) : ResponseEntity.notFound().build();
}
@GetMapping("/orders/user/{userId}")
public List<Order> getOrdersByUser(@PathVariable Long userId) {
return orderService.findByUserId(userId);
}
/**
* 创建订单 - 演示事务
*/
@PostMapping("/orders")
public ResponseEntity<?> createOrder(@Valid @RequestBody OrderCreateRequest request) {
try {
Order order = orderService.createOrderWithRollback(
request.getUserId(),
request.getProductId(),
request.getQuantity(),
request.isRollback()
);
return ResponseEntity.ok(Map.of(
"success", true,
"order", order,
"message", request.isRollback() ? "事务已回滚但订单仍创建REQUIRES_NEW演示" : "订单创建成功"
));
} catch (RuntimeException e) {
return ResponseEntity.badRequest().body(Map.of(
"success", false,
"error", e.getMessage()
));
}
}
@PatchMapping("/orders/{id}/status")
public ResponseEntity<Void> updateOrderStatus(@PathVariable Long id, @RequestParam String status) {
orderService.updateStatus(id, status);
return ResponseEntity.ok().build();
}
@DeleteMapping("/orders/{id}")
public ResponseEntity<Void> deleteOrder(@PathVariable Long id) {
orderService.deleteById(id);
return ResponseEntity.noContent().build();
}
}

View File

@@ -0,0 +1,13 @@
package com.example.scaffold.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class RootController {
@GetMapping("/")
public String root() {
return "Spring Boot Scaffold is running!";
}
}

View File

@@ -0,0 +1,100 @@
package com.example.scaffold.controller;
import com.example.scaffold.dto.UserCreateRequest;
import com.example.scaffold.entity.User;
import com.example.scaffold.service.UserService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
/**
* 用户控制器 - RESTful API 演示
*/
@Slf4j
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
/**
* GET - 查询所有用户
*/
@GetMapping
public List<User> list() {
return userService.findAll();
}
/**
* GET - 根据 ID 查询
*/
@GetMapping("/{id}")
public ResponseEntity<User> getById(@PathVariable Long id) {
return userService.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
/**
* GET - 搜索用户
*/
@GetMapping("/search")
public List<User> search(@RequestParam String username) {
return userService.searchByUsername(username);
}
/**
* POST - 创建用户
*/
@PostMapping
public ResponseEntity<User> create(@Valid @RequestBody UserCreateRequest request) {
User user = new User();
user.setUsername(request.getUsername());
user.setEmail(request.getEmail());
user.setPhone(request.getPhone());
user.setBio(request.getBio());
user.setActive(true);
User saved = userService.save(user);
return ResponseEntity.ok(saved);
}
/**
* PUT - 更新用户
*/
@PutMapping("/{id}")
public ResponseEntity<User> update(@PathVariable Long id, @Valid @RequestBody UserCreateRequest request) {
return userService.findById(id)
.map(existing -> {
existing.setUsername(request.getUsername());
existing.setEmail(request.getEmail());
existing.setPhone(request.getPhone());
existing.setBio(request.getBio());
return ResponseEntity.ok(userService.save(existing));
})
.orElse(ResponseEntity.notFound().build());
}
/**
* DELETE - 删除用户
*/
@DeleteMapping("/{id}")
public ResponseEntity<Void> delete(@PathVariable Long id) {
userService.deleteById(id);
return ResponseEntity.noContent().build();
}
/**
* GET - 统计
*/
@GetMapping("/count")
public Map<String, Long> count() {
return Map.of("count", userService.count());
}
}

View File

@@ -0,0 +1,25 @@
package com.example.scaffold.dto;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* 订单创建请求 DTO
*/
@Data
public class OrderCreateRequest {
@NotNull(message = "用户ID不能为空")
private Long userId;
@NotNull(message = "产品ID不能为空")
private Long productId;
@NotNull(message = "数量不能为空")
@Min(value = 1, message = "数量至少为1")
private Integer quantity;
/** 是否触发回滚(用于演示事务) */
private boolean rollback = false;
}

View File

@@ -0,0 +1,28 @@
package com.example.scaffold.dto;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.math.BigDecimal;
/**
* 产品创建请求 DTO
*/
@Data
public class ProductCreateRequest {
@NotNull(message = "产品名称不能为空")
private String name;
private String description;
@NotNull(message = "价格不能为空")
@Min(value = 0, message = "价格不能为负数")
private BigDecimal price;
@Min(value = 0, message = "库存不能为负数")
private Integer stockQuantity = 0;
private String category;
}

View File

@@ -0,0 +1,33 @@
package com.example.scaffold.dto;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Data;
/**
* 用户创建请求 DTO
*
* 学习要点:
* 1. @Data - Lombok 自动生成 getter/setter
* 2. @NotBlank - 验证非空
* 3. @Size - 验证长度
* 4. @Email - 验证邮箱格式
*/
@Data
public class UserCreateRequest {
@NotBlank(message = "用户名不能为空")
@Size(min = 2, max = 50, message = "用户名长度必须在2-50之间")
private String username;
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;
@Size(max = 20, message = "手机号最长20位")
private String phone;
@Size(max = 500, message = "简介最长500字")
private String bio;
}

View File

@@ -0,0 +1,42 @@
package com.example.scaffold.entity;
import jakarta.persistence.*;
import lombok.Data;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 订单实体 - 用于演示事务传播
*/
@Data
@Entity
@Table(name = "orders")
@EntityListeners(AuditingEntityListener.class)
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "user_id", nullable = false)
private Long userId;
@Column(name = "product_id", nullable = false)
private Long productId;
@Column(nullable = false)
private Integer quantity = 1;
@Column(name = "total_price", nullable = false, precision = 10, scale = 2)
private BigDecimal totalPrice;
@Column(length = 20)
private String status = "PENDING"; // PENDING, PAID, SHIPPED, COMPLETED, CANCELLED
@Column(name = "created_at", updatable = false)
@CreatedDate
private LocalDateTime createdAt;
}

View File

@@ -0,0 +1,42 @@
package com.example.scaffold.entity;
import jakarta.persistence.*;
import lombok.Data;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 产品实体 - 用于演示事务和复杂查询
*/
@Data
@Entity
@Table(name = "products")
@EntityListeners(AuditingEntityListener.class)
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 100)
private String name;
@Column(length = 500)
private String description;
@Column(nullable = false, precision = 10, scale = 2)
private BigDecimal price;
@Column(name = "stock_quantity")
private Integer stockQuantity = 0;
@Column(length = 50)
private String category;
@Column(name = "created_at", updatable = false)
@CreatedDate
private LocalDateTime createdAt;
}

View File

@@ -0,0 +1,101 @@
package com.example.scaffold.entity;
import jakarta.persistence.*;
import lombok.Data;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import java.time.LocalDateTime;
/**
* 用户实体 - 用于演示 JPA 和 MyBatis
*
* 学习要点:
* 1. @Entity - 标记为 JPA 实体
* 2. @Table - 指定表名
* 3. @Id + @GeneratedValue - 主键策略
* 4. @Column - 列映射
* 5. @Data - Lombok 自动生成 getter/setter
*/
@Data
@Entity
@Table(name = "users")
@EntityListeners(AuditingEntityListener.class)
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 50)
private String username;
@Column(nullable = false, length = 100)
private String email;
@Column(length = 20)
private String phone;
@Column(columnDefinition = "TEXT")
private String bio;
@Column(nullable = false)
private Boolean active = true;
@CreatedDate
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
@LastModifiedDate
@Column(name = "updated_at")
private LocalDateTime updatedAt;
/**
* 生命周期回调 - 持久化前
*/
@PrePersist
public void prePersist() {
System.out.println("🔔 [User @PrePersist] 即将保存用户: " + username);
}
/**
* 生命周期回调 - 持久化后
*/
@PostPersist
public void postPersist() {
System.out.println("✅ [User @PostPersist] 用户已保存, ID: " + id);
}
/**
* 生命周期回调 - 更新前
*/
@PreUpdate
public void preUpdate() {
System.out.println("🔔 [User @PreUpdate] 即将更新用户: " + id);
}
/**
* 生命周期回调 - 更新后
*/
@PostUpdate
public void postUpdate() {
System.out.println("✅ [User @PostUpdate] 用户已更新: " + id);
}
/**
* 生命周期回调 - 删除前
*/
@PreRemove
public void preRemove() {
System.out.println("🔔 [User @PreRemove] 即将删除用户: " + id);
}
/**
* 生命周期回调 - 删除后
*/
@PostRemove
public void postRemove() {
System.out.println("✅ [User @PostRemove] 用户已删除: " + id);
}
}

View File

@@ -0,0 +1,110 @@
package com.example.scaffold.learning;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.util.*;
/**
* AOP 学习控制器
*
* 学习要点:
* 1. 切面概念
* 2. 通知类型
* 3. 切入点表达式
*/
@Slf4j
@RestController
@RequestMapping("/api/learning/aop")
@RequiredArgsConstructor
public class AopLearningController {
/**
* AOP 概念说明
*/
@GetMapping("/concepts")
public Map<String, Object> concepts() {
return Map.of(
"AOP", "面向切面编程 (Aspect Oriented Programming)",
"核心概念", Map.of(
"Aspect (切面)", "横切关注点的模块化",
"JoinPoint (连接点)", "程序执行的某个特定位置",
"Pointcut (切入点)", "匹配连接点的表达式",
"Advice (通知)", "在切入点执行的代码",
"Target (目标对象)", "被通知的对象",
"Proxy (代理)", "AOP 创建的代理对象",
"Weaving (织入)", "将切面应用到目标对象的过程"
),
"通知类型", Map.of(
"@Before", "方法执行前",
"@After", "方法执行后(无论是否异常)",
"@AfterReturning", "方法成功返回后",
"@AfterThrowing", "方法抛出异常后",
"@Around", "环绕 - 完全控制方法执行"
)
);
}
/**
* 切入点表达式语法
*/
@GetMapping("/pointcut-syntax")
public Map<String, Object> pointcutSyntax() {
return Map.of(
"语法", "execution(修饰符? 返回类型 包名.类名.方法名(参数) 异常?)",
"示例", List.of(
"execution(* com.example.service.*.*(..)) - service包下所有方法",
"execution(* com.example.service..*.*(..)) - service包及子包所有方法",
"execution(public * *(..)) - 所有public方法",
"execution(* set*(..)) - 所有set开头的方法",
"execution(* com.example.service.UserService.*(..)) - UserService的所有方法",
"@annotation(org.springframework.transaction.annotation.Transactional) - 带@Transactional的方法"
),
"通配符", Map.of(
"*", "匹配任意字符",
"..", "匹配任意层级的包或任意参数",
"+", "匹配指定类及其子类"
)
);
}
/**
* 测试 AOP - 这个方法会被切面拦截
*/
@GetMapping("/test")
public Map<String, Object> testAop(@RequestParam(defaultValue = "test") String message) {
log.info("📝 [AopLearningController] 测试方法被调用: message={}", message);
return Map.of(
"message", "AOP 测试成功",
"input", message,
"tip", "查看控制台日志,观察 AOP 通知的执行顺序",
"expectedOrder", List.of(
"1. @Around 开始",
"2. @Before",
"3. 方法执行",
"4. @AfterReturning",
"5. @After",
"6. @Around 结束"
)
);
}
/**
* 演示异常通知
*/
@GetMapping("/test-error")
public Map<String, Object> testError(@RequestParam(defaultValue = "false") boolean error) {
log.info("📝 [AopLearningController] 测试异常通知: error={}", error);
if (error) {
throw new RuntimeException("这是一个测试异常,用于触发 @AfterThrowing");
}
return Map.of(
"message", "正常执行",
"tip", "传入 error=true 触发异常,观察 @AfterThrowing"
);
}
}

View File

@@ -0,0 +1,164 @@
package com.example.scaffold.learning;
import com.example.scaffold.aop.PerformanceAspect;
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.*;
import org.springframework.web.context.WebApplicationContext;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import java.util.*;
/**
* IoC 容器学习控制器
*
* 学习要点:
* 1. Bean 的生命周期
* 2. 依赖注入方式
* 3. Bean 作用域
* 4. 条件化配置
*/
@RestController
@RequestMapping("/api/learning/ioc")
@RequiredArgsConstructor
public class IocLearningController {
private final ApplicationContext applicationContext;
private final PerformanceAspect performanceAspect;
// 演示字段注入(不推荐,但可以用)
@Autowired
@Qualifier("learningBean")
private LearningBean learningBean;
/**
* 查看所有 Bean
*/
@GetMapping("/beans")
public Map<String, Object> listBeans() {
String[] beanNames = applicationContext.getBeanDefinitionNames();
Map<String, Object> result = new LinkedHashMap<>();
result.put("total", beanNames.length);
result.put("userBeans", Arrays.stream(beanNames)
.filter(name -> name.startsWith("user") || name.startsWith("learning") ||
name.contains("Service") || name.contains("Controller") || name.contains("Mapper"))
.sorted()
.toList());
result.put("allBeans", Arrays.stream(beanNames).sorted().toList());
return result;
}
/**
* 查看 Bean 详情
*/
@GetMapping("/beans/{name}")
public Map<String, Object> getBeanDetail(@PathVariable String name) {
try {
Object bean = applicationContext.getBean(name);
Class<?> clazz = bean.getClass();
return Map.of(
"name", name,
"type", clazz.getName(),
"simpleName", clazz.getSimpleName(),
"interfaces", Arrays.toString(clazz.getInterfaces()),
"annotations", Arrays.toString(clazz.getAnnotations()),
"scope", applicationContext.isSingleton(name) ? "singleton" : "prototype"
);
} catch (BeansException e) {
return Map.of("error", "Bean not found: " + name);
}
}
/**
* 演示依赖注入方式
*/
@GetMapping("/injection-types")
public Map<String, String> injectionTypes() {
return Map.of(
"构造器注入", "推荐!明确依赖,不可变,易于测试",
"Setter注入", "可选依赖,灵活性高",
"字段注入", "不推荐!隐藏依赖,难以测试",
"本控制器使用", "构造器注入 (RequiredArgsConstructor) + 字段注入演示"
);
}
/**
* 演示 Bean 作用域
*/
@GetMapping("/scopes")
public Map<String, Object> scopes() {
return Map.of(
"singleton", "单例 - 整个应用只有一个实例(默认)",
"prototype", "原型 - 每次请求都创建新实例",
"request", "请求 - 每个 HTTP 请求一个实例",
"session", "会话 - 每个 HTTP 会话一个实例",
"demo", learningBean.getInstanceInfo()
);
}
/**
* 性能统计
*/
@GetMapping("/performance")
public Map<String, Object> getPerformance() {
var stats = performanceAspect.getStats();
Map<String, Object> result = new LinkedHashMap<>();
stats.forEach((key, value) -> {
long totalMs = value.totalTime.get() / 1_000_000;
long avgMs = value.totalCount.get() > 0 ? totalMs / value.totalCount.get() : 0;
result.put(key, Map.of(
"count", value.totalCount.get(),
"errors", value.errorCount.get(),
"totalMs", totalMs,
"avgMs", avgMs,
"maxMs", value.maxTime.get()
));
});
return result;
}
/**
* 重置性能统计
*/
@PostMapping("/performance/reset")
public Map<String, String> resetPerformance() {
performanceAspect.resetStats();
return Map.of("status", "ok", "message", "性能统计已重置");
}
/**
* 学习 Bean - 演示作用域和生命周期
*/
@org.springframework.stereotype.Component("learningBean")
@Scope(WebApplicationContext.SCOPE_SESSION)
public static class LearningBean {
private final String instanceId = UUID.randomUUID().toString().substring(0, 8);
private int accessCount = 0;
@PostConstruct
public void init() {
System.out.println("🟢 [LearningBean @PostConstruct] Bean 初始化: " + instanceId);
}
@PreDestroy
public void destroy() {
System.out.println("🔴 [LearningBean @PreDestroy] Bean 销毁: " + instanceId);
}
public String getInstanceInfo() {
accessCount++;
return String.format("实例ID: %s, 访问次数: %d, 作用域: session", instanceId, accessCount);
}
}
}

View File

@@ -0,0 +1,127 @@
package com.example.scaffold.learning;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.web.bind.annotation.*;
import java.util.*;
/**
* MyBatis 学习控制器
*
* 学习要点:
* 1. MyBatis vs JPA 对比
* 2. 动态 SQL
* 3. 缓存机制
* 4. 结果映射
*/
@Slf4j
@RestController
@RequestMapping("/api/learning/mybatis")
@RequiredArgsConstructor
public class MyBatisLearningController {
private final SqlSessionFactory sqlSessionFactory;
/**
* MyBatis 核心概念
*/
@GetMapping("/concepts")
public Map<String, Object> concepts() {
return Map.of(
"MyBatis", "半自动 ORM 框架SQL 与 Java 对象映射",
"核心组件", Map.of(
"SqlSessionFactory", "创建 SqlSession 的工厂",
"SqlSession", "执行 SQL 的会话",
"Mapper", "接口绑定的 SQL 语句",
"Configuration", "MyBatis 配置信息"
),
"缓存", Map.of(
"一级缓存", "SqlSession 级别,默认开启",
"二级缓存", "Mapper 级别,需配置 @CacheNamespace"
)
);
}
/**
* MyBatis vs JPA 对比
*/
@GetMapping("/vs-jpa")
public Map<String, Object> vsJpa() {
return Map.of(
"MyBatis", Map.of(
"优点", List.of("SQL 灵活可控", "性能优化方便", "复杂查询友好"),
"缺点", List.of("SQL 与代码耦合", "数据库迁移成本高", "需要手写 SQL"),
"适用场景", "复杂查询、性能要求高、DBA 参与项目"
),
"JPA/Hibernate", Map.of(
"优点", List.of("面向对象", "数据库无关", "开发效率高"),
"缺点", List.of("复杂查询困难", "性能调优复杂", "学习曲线陡"),
"适用场景", "标准 CRUD、快速开发、领域驱动设计"
),
"建议", "小型项目用 JPA大型/复杂查询项目用 MyBatis"
);
}
/**
* 动态 SQL 语法
*/
@GetMapping("/dynamic-sql")
public Map<String, Object> dynamicSql() {
return Map.of(
"if", "条件判断 <if test='name != null'> AND name = #{name} </if>",
"choose/when/otherwise", "类似 switch-case",
"trim/where/set", "处理 SQL 拼接",
"foreach", "循环遍历 <foreach item='item' collection='list' separator=','>#{item}</foreach>",
"bind", "定义变量 <bind name='pattern' value=\"'%' + name + '%'\" />",
"示例", """
<select id='findUser'>
SELECT * FROM users
<where>
<if test='name != null'>AND name LIKE #{name}</if>
<if test='email != null'>AND email = #{email}</if>
</where>
</select>
"""
);
}
/**
* 缓存演示
*/
@GetMapping("/cache")
public Map<String, Object> cacheDemo() {
return Map.of(
"一级缓存", Map.of(
"范围", "SqlSession 级别",
"默认", "开启",
"失效条件", List.of("执行 insert/update/delete", "调用 sqlSession.clearCache()", "事务提交/回滚"),
"演示", "同一 SqlSession 内连续两次相同查询,第二次不执行 SQL"
),
"二级缓存", Map.of(
"范围", "Mapper 级别",
"配置", "@CacheNamespace 或 XML 配置",
"注意", "实体类需要实现 Serializable",
"本项目", "UserMapper 已启用二级缓存"
),
"验证方式", "查看控制台 SQL 日志,缓存命中时不打印 SQL"
);
}
/**
* 查看当前配置
*/
@GetMapping("/config")
public Map<String, Object> getConfig() {
var config = sqlSessionFactory.getConfiguration();
return Map.of(
"cacheEnabled", config.isCacheEnabled(),
"localCacheScope", config.getLocalCacheScope().name(),
"defaultExecutorType", config.getDefaultExecutorType().name(),
"mapUnderscoreToCamelCase", config.isMapUnderscoreToCamelCase(),
"mappedStatements", config.getMappedStatements().size()
);
}
}

View File

@@ -0,0 +1,135 @@
package com.example.scaffold.learning;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.util.*;
/**
* 事务学习控制器
*
* 学习要点:
* 1. 事务传播行为
* 2. 事务隔离级别
* 3. 事务回滚
*/
@Slf4j
@RestController
@RequestMapping("/api/learning/transaction")
@RequiredArgsConstructor
public class TransactionLearningController {
/**
* 事务概念
*/
@GetMapping("/concepts")
public Map<String, Object> concepts() {
return Map.of(
"ACID", Map.of(
"Atomicity (原子性)", "事务是不可分割的工作单位",
"Consistency (一致性)", "事务必须使数据库从一个一致性状态变换到另一个一致性状态",
"Isolation (隔离性)", "多个用户并发访问数据库时,数据库为每个用户开启的事务不能被其他事务干扰",
"Durability (持久性)", "事务一旦提交,对数据库的改变是永久性的"
),
"Spring事务", "@Transactional 注解实现声明式事务管理"
);
}
/**
* 传播行为详解
*/
@GetMapping("/propagation")
public Map<String, Object> propagation() {
return Map.of(
"REQUIRED (默认)", Map.of(
"描述", "有事务则加入,无则新建",
"场景", "最常用,大多数业务方法",
"示例", "A 调用 BB 加入 A 的事务"
),
"REQUIRES_NEW", Map.of(
"描述", "总是新建事务,挂起当前事务",
"场景", "日志记录、独立子任务",
"示例", "A 调用 BB 在新事务执行A 回滚不影响 B"
),
"SUPPORTS", Map.of(
"描述", "有事务则加入,无则以非事务运行",
"场景", "查询方法"
),
"NOT_SUPPORTED", Map.of(
"描述", "以非事务运行,挂起当前事务",
"场景", "不需要事务的操作"
),
"MANDATORY", Map.of(
"描述", "必须在事务中运行,否则抛异常",
"场景", "强制要求事务"
),
"NEVER", Map.of(
"描述", "不能在事务中运行,否则抛异常",
"场景", "确保无事务"
),
"NESTED", Map.of(
"描述", "嵌套事务,可独立回滚",
"场景", "部分失败不影响整体"
)
);
}
/**
* 隔离级别详解
*/
@GetMapping("/isolation")
public Map<String, Object> isolation() {
return Map.of(
"DEFAULT", "使用数据库默认隔离级别",
"READ_UNCOMMITTED", Map.of(
"描述", "读未提交",
"问题", "脏读、不可重复读、幻读",
"性能", "最高"
),
"READ_COMMITTED", Map.of(
"描述", "读已提交",
"问题", "不可重复读、幻读",
"性能", "较高",
"场景", "大多数数据库默认"
),
"REPEATABLE_READ", Map.of(
"描述", "可重复读",
"问题", "幻读",
"性能", "中等",
"场景", "MySQL 默认"
),
"SERIALIZABLE", Map.of(
"描述", "串行化",
"问题", "",
"性能", "最低",
"场景", "数据一致性要求极高"
),
"问题说明", Map.of(
"脏读", "读到其他事务未提交的数据",
"不可重复读", "同一事务两次读取结果不同(修改导致)",
"幻读", "同一事务两次读取结果不同(新增/删除导致)"
)
);
}
/**
* 回滚规则
*/
@GetMapping("/rollback")
public Map<String, Object> rollback() {
return Map.of(
"默认行为", "只对 RuntimeException 和 Error 回滚",
"rollbackFor", "指定需要回滚的异常类型",
"noRollbackFor", "指定不需要回滚的异常类型",
"示例", """
@Transactional(rollbackFor = Exception.class)
@Transactional(noRollbackFor = BusinessException.class)
""",
"测试API", Map.of(
"创建订单", "POST /api/orders",
"创建订单(回滚)", "POST /api/orders?rollback=true"
)
);
}
}

View File

@@ -0,0 +1,33 @@
package com.example.scaffold.mapper;
import com.example.scaffold.entity.Order;
import org.apache.ibatis.annotations.*;
import java.util.List;
/**
* Order MyBatis Mapper
*/
@Mapper
public interface OrderMapper {
@Select("SELECT * FROM orders ORDER BY created_at DESC")
List<Order> findAll();
@Select("SELECT * FROM orders WHERE id = #{id}")
Order findById(@Param("id") Long id);
@Select("SELECT * FROM orders WHERE user_id = #{userId}")
List<Order> findByUserId(@Param("userId") Long userId);
@Insert("INSERT INTO orders(user_id, product_id, quantity, total_price, status, created_at) " +
"VALUES(#{userId}, #{productId}, #{quantity}, #{totalPrice}, #{status}, NOW())")
@Options(useGeneratedKeys = true, keyProperty = "id")
int insert(Order order);
@Update("UPDATE orders SET status = #{status} WHERE id = #{id}")
int updateStatus(@Param("id") Long id, @Param("status") String status);
@Delete("DELETE FROM orders WHERE id = #{id}")
int deleteById(@Param("id") Long id);
}

View File

@@ -0,0 +1,40 @@
package com.example.scaffold.mapper;
import com.example.scaffold.entity.Product;
import org.apache.ibatis.annotations.*;
import java.math.BigDecimal;
import java.util.List;
/**
* Product MyBatis Mapper - 演示复杂查询
*/
@Mapper
public interface ProductMapper {
@Select("SELECT * FROM products ORDER BY created_at DESC")
List<Product> findAll();
@Select("SELECT * FROM products WHERE id = #{id}")
Product findById(@Param("id") Long id);
@Select("SELECT * FROM products WHERE category = #{category}")
List<Product> findByCategory(@Param("category") String category);
@Select("SELECT * FROM products WHERE price BETWEEN #{min} AND #{max} ORDER BY price")
List<Product> findByPriceRange(@Param("min") BigDecimal min, @Param("max") BigDecimal max);
@Insert("INSERT INTO products(name, description, price, stock_quantity, category, created_at) " +
"VALUES(#{name}, #{description}, #{price}, #{stockQuantity}, #{category}, NOW())")
@Options(useGeneratedKeys = true, keyProperty = "id")
int insert(Product product);
@Update("UPDATE products SET stock_quantity = stock_quantity - #{quantity} WHERE id = #{id} AND stock_quantity >= #{quantity}")
int decreaseStock(@Param("id") Long id, @Param("quantity") Integer quantity);
@Update("UPDATE products SET stock_quantity = stock_quantity + #{quantity} WHERE id = #{id}")
int increaseStock(@Param("id") Long id, @Param("quantity") Integer quantity);
@Delete("DELETE FROM products WHERE id = #{id}")
int deleteById(@Param("id") Long id);
}

View File

@@ -0,0 +1,78 @@
package com.example.scaffold.mapper;
import com.example.scaffold.entity.User;
import org.apache.ibatis.annotations.*;
import org.apache.ibatis.cache.decorators.LruCache;
import java.util.List;
/**
* User MyBatis Mapper - 演示 MyBatis 注解方式
*
* 学习要点:
* 1. @Mapper - 标记为 MyBatis Mapper 接口
* 2. @Select/@Insert/@Update/@Delete - SQL 注解
* 3. @Options - 额外选项如返回自增ID
* 4. @ResultMap - 结果映射
* 5. @CacheNamespace - 二级缓存
*/
@Mapper
@CacheNamespace(implementation = LruCache.class, size = 1024)
public interface UserMapper {
/**
* 查询所有用户
*/
@Select("SELECT * FROM users ORDER BY created_at DESC")
List<User> findAll();
/**
* 根据ID查询 - 使用一级缓存
*/
@Select("SELECT * FROM users WHERE id = #{id}")
User findById(@Param("id") Long id);
/**
* 根据用户名模糊查询 - 动态SQL演示
*/
@Select("SELECT * FROM users WHERE username LIKE CONCAT('%', #{username}, '%')")
List<User> findByUsernameLike(@Param("username") String username);
/**
* 插入用户 - 返回自增ID
*/
@Insert("INSERT INTO users(username, email, phone, bio, active, created_at, updated_at) " +
"VALUES(#{username}, #{email}, #{phone}, #{bio}, #{active}, NOW(), NOW())")
@Options(useGeneratedKeys = true, keyProperty = "id")
int insert(User user);
/**
* 更新用户
*/
@Update("UPDATE users SET username=#{username}, email=#{email}, phone=#{phone}, " +
"bio=#{bio}, active=#{active}, updated_at=NOW() WHERE id=#{id}")
int update(User user);
/**
* 删除用户
*/
@Delete("DELETE FROM users WHERE id = #{id}")
int deleteById(@Param("id") Long id);
/**
* 统计用户数量
*/
@Select("SELECT COUNT(*) FROM users")
long count();
/**
* 批量插入 - 演示脚本SQL
*/
@Insert("<script>" +
"INSERT INTO users(username, email, phone, active, created_at, updated_at) VALUES " +
"<foreach collection='users' item='u' separator=','>" +
"(#{u.username}, #{u.email}, #{u.phone}, #{u.active}, NOW(), NOW())" +
"</foreach>" +
"</script>")
int batchInsert(@Param("users") List<User> users);
}

View File

@@ -0,0 +1,24 @@
package com.example.scaffold.service;
import com.example.scaffold.entity.User;
import java.util.List;
import java.util.Optional;
/**
* 用户服务接口 - 演示接口与实现分离
*/
public interface UserService {
List<User> findAll();
Optional<User> findById(Long id);
User save(User user);
void deleteById(Long id);
List<User> searchByUsername(String username);
long count();
}

View File

@@ -0,0 +1,126 @@
package com.example.scaffold.service.impl;
import com.example.scaffold.entity.Order;
import com.example.scaffold.entity.Product;
import com.example.scaffold.mapper.OrderMapper;
import com.example.scaffold.mapper.ProductMapper;
import com.example.scaffold.mapper.UserMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.List;
/**
* 订单服务 - 演示事务传播和隔离级别
*
* 学习要点:
* 1. 事务传播行为 - Propagation
* 2. 事务隔离级别 - Isolation
* 3. 事务回滚 - rollbackFor
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderMapper orderMapper;
private final ProductMapper productMapper;
private final UserMapper userMapper;
/**
* 创建订单 - 演示事务
* REQUIRED: 有事务则加入,无则新建
*/
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public Order createOrder(Long userId, Long productId, Integer quantity) {
log.info("📦 [OrderService] 创建订单: userId={}, productId={}, quantity={}", userId, productId, quantity);
// 检查用户
if (userMapper.findById(userId) == null) {
throw new RuntimeException("用户不存在: " + userId);
}
// 检查产品
Product product = productMapper.findById(productId);
if (product == null) {
throw new RuntimeException("产品不存在: " + productId);
}
// 扣减库存
int rows = productMapper.decreaseStock(productId, quantity);
if (rows == 0) {
throw new RuntimeException("库存不足");
}
// 创建订单
Order order = new Order();
order.setUserId(userId);
order.setProductId(productId);
order.setQuantity(quantity);
order.setTotalPrice(product.getPrice().multiply(BigDecimal.valueOf(quantity)));
order.setStatus("PENDING");
orderMapper.insert(order);
log.info("✅ [OrderService] 订单创建成功: orderId={}", order.getId());
return order;
}
/**
* 模拟事务回滚
*/
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public Order createOrderWithRollback(Long userId, Long productId, Integer quantity, boolean shouldRollback) {
log.info("📦 [OrderService] 创建订单(可能回滚): userId={}, shouldRollback={}", userId, shouldRollback);
Order order = createOrder(userId, productId, quantity);
if (shouldRollback) {
log.warn("⚠️ [OrderService] 触发回滚!");
throw new RuntimeException("模拟事务回滚");
}
return order;
}
/**
* REQUIRES_NEW - 挂起当前事务,创建新事务
*/
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logOrderOperation(Long orderId, String operation) {
log.info("📝 [OrderService] 记录订单操作: orderId={}, operation={}", orderId, operation);
// 这个方法会在独立事务中执行
// 即使外部事务回滚,这里的记录也会保留
}
/**
* 使用隔离级别 READ_COMMITTED
*/
@Transactional(isolation = Isolation.READ_COMMITTED)
public Order getOrder(Long id) {
return orderMapper.findById(id);
}
public List<Order> findAll() {
return orderMapper.findAll();
}
public List<Order> findByUserId(Long userId) {
return orderMapper.findByUserId(userId);
}
@Transactional
public void updateStatus(Long id, String status) {
orderMapper.updateStatus(id, status);
}
@Transactional
public void deleteById(Long id) {
orderMapper.deleteById(id);
}
}

View File

@@ -0,0 +1,77 @@
package com.example.scaffold.service.impl;
import com.example.scaffold.entity.User;
import com.example.scaffold.mapper.UserMapper;
import com.example.scaffold.service.UserService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
/**
* 用户服务实现 - 演示 Spring 服务层
*
* 学习要点:
* 1. @Service - 标记为服务组件
* 2. @Transactional - 声明式事务
* 3. @RequiredArgsConstructor - Lombok 构造器注入
* 4. @Slf4j - Lombok 日志
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {
private final UserMapper userMapper;
@Override
public List<User> findAll() {
log.info("📊 [UserService] 查询所有用户");
return userMapper.findAll();
}
@Override
public Optional<User> findById(Long id) {
log.info("🔍 [UserService] 查询用户: id={}", id);
// 演示 MyBatis 一级缓存 - 连续两次查询
User user = userMapper.findById(id);
User cached = userMapper.findById(id); // 第二次会命中缓存
if (cached != null) {
log.info("✅ [UserService] 一级缓存命中: id={}", id);
}
return Optional.ofNullable(user);
}
@Override
@Transactional
public User save(User user) {
log.info("💾 [UserService] 保存用户: {}", user.getUsername());
if (user.getId() == null) {
userMapper.insert(user);
} else {
userMapper.update(user);
}
return user;
}
@Override
@Transactional
public void deleteById(Long id) {
log.info("🗑️ [UserService] 删除用户: id={}", id);
userMapper.deleteById(id);
}
@Override
public List<User> searchByUsername(String username) {
log.info("🔎 [UserService] 搜索用户: username={}", username);
return userMapper.findByUsernameLike(username);
}
@Override
public long count() {
return userMapper.count();
}
}

View File

@@ -0,0 +1,22 @@
spring.application.name=springboot-scaffold
server.port=8082
# H2 Database
spring.h2.console.enabled=true
spring.datasource.url=jdbc:h2:file:~/h2/springboot_scaffold
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
# JPA
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
# MyBatis
mybatis.configuration.map-underscore-to-camel-case=true
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
# Logging
logging.level.com.example.scaffold=DEBUG
logging.level.org.springframework.transaction.interceptor=TRACE

View File

@@ -0,0 +1,159 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AOP 切面学习 - Spring Boot</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; }
.container { max-width: 1400px; margin: 0 auto; padding: 20px; }
.header { background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); color: white; padding: 30px 20px; text-align: center; margin-bottom: 20px; border-radius: 10px; }
.header h1 { font-size: 2em; }
.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:hover, .nav a.active { background: #f5576c; 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 h3 { color: #f5576c; margin-bottom: 15px; border-bottom: 2px solid #eee; padding-bottom: 10px; }
.advice-box { background: #f8f9fa; border-radius: 8px; padding: 15px; margin: 10px 0; border-left: 4px solid; }
.advice-before { border-color: #28a745; }
.advice-after { border-color: #6c757d; }
.advice-returning { border-color: #17a2b8; }
.advice-throwing { border-color: #dc3545; }
.advice-around { border-color: #ffc107; }
.btn { padding: 10px 20px; background: #f5576c; color: white; border: none; border-radius: 5px; cursor: pointer; margin: 5px; }
.btn:hover { background: #e0465b; }
.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; }
.code-block { background: #f4f4f4; padding: 15px; border-radius: 5px; font-family: monospace; font-size: 0.9em; overflow-x: auto; margin: 10px 0; }
.flow-diagram { display: flex; align-items: center; justify-content: center; flex-wrap: wrap; gap: 10px; padding: 20px; background: #f8f9fa; border-radius: 10px; margin: 15px 0; }
.flow-step { padding: 10px 20px; background: white; border-radius: 20px; border: 2px solid #ddd; }
.flow-arrow { font-size: 1.5em; color: #999; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🔪 AOP 切面编程</h1>
<p>Aspect Oriented Programming - 面向切面编程</p>
</div>
<div class="nav">
<a href="index.html">🏠 首页</a>
<a href="ioc.html">📦 IoC</a>
<a href="aop.html" class="active">🔪 AOP</a>
<a href="mybatis.html">💾 MyBatis</a>
<a href="transaction.html">🔄 事务</a>
<a href="users.html">👥 用户</a>
<a href="api.html">🔌 API</a>
</div>
<div class="card">
<h3>📚 核心概念</h3>
<p><strong>AOP (面向切面编程)</strong>:将横切关注点(日志、权限、事务等)从业务逻辑中分离出来,实现模块化。</p>
<div class="flow-diagram">
<div class="flow-step">目标方法</div>
<div class="flow-arrow"></div>
<div class="flow-step">@Before</div>
<div class="flow-arrow"></div>
<div class="flow-step">@Around (前)</div>
<div class="flow-arrow"></div>
<div class="flow-step">方法执行</div>
<div class="flow-arrow"></div>
<div class="flow-step">@Around (后)</div>
<div class="flow-arrow"></div>
<div class="flow-step">@AfterReturning</div>
<div class="flow-arrow"></div>
<div class="flow-step">@After</div>
</div>
</div>
<div class="card">
<h3>🔔 五种通知类型</h3>
<div class="advice-box advice-before">
<h4>@Before - 前置通知</h4>
<p>方法执行前触发,可用于参数校验、日志记录</p>
<div class="code-block">@Before("execution(* com.example.service..*.*(..))")<br>public void before(JoinPoint jp) {<br> log.info("即将执行: " + jp.getSignature().getName());<br>}</div>
</div>
<div class="advice-box advice-after">
<h4>@After - 后置通知</h4>
<p>方法执行后触发(无论是否异常),可用于资源释放</p>
<div class="code-block">@After("execution(* com.example.service..*.*(..))")<br>public void after(JoinPoint jp) {<br> log.info("执行完成: " + jp.getSignature().getName());<br>}</div>
</div>
<div class="advice-box advice-returning">
<h4>@AfterReturning - 返回通知</h4>
<p>方法成功返回后触发,可获取返回值</p>
<div class="code-block">@AfterReturning(pointcut="...", returning="result")<br>public void afterReturning(Object result) {<br> log.info("返回结果: " + result);<br>}</div>
</div>
<div class="advice-box advice-throwing">
<h4>@AfterThrowing - 异常通知</h4>
<p>方法抛出异常后触发,可用于异常处理</p>
<div class="code-block">@AfterThrowing(pointcut="...", throwing="ex")<br>public void afterThrowing(Exception ex) {<br> log.error("发生异常: " + ex.getMessage());<br>}</div>
</div>
<div class="advice-box advice-around">
<h4>@Around - 环绕通知</h4>
<p>完全控制方法执行,可决定是否执行目标方法</p>
<div class="code-block">@Around("execution(* com.example.controller..*.*(..))")<br>public Object around(ProceedingJoinPoint pjp) throws Throwable {<br> long start = System.currentTimeMillis();<br> Object result = pjp.proceed(); // 执行目标方法<br> long cost = System.currentTimeMillis() - start;<br> log.info("耗时: " + cost + "ms");<br> return result;<br>}</div>
</div>
</div>
<div class="card">
<h3>🧪 在线测试</h3>
<p>调用下面的 API然后查看控制台日志观察 AOP 执行顺序</p>
<button class="btn" onclick="testAop()">测试正常执行</button>
<button class="btn" onclick="testAopError()">测试异常通知</button>
<div id="testResult" class="result"></div>
</div>
<div class="card">
<h3>📝 切入点表达式</h3>
<table style="width:100%;border-collapse:collapse;">
<tr style="background:#f8f9fa;"><th style="padding:10px;text-align:left;">表达式</th><th style="padding:10px;text-align:left;">含义</th></tr>
<tr><td style="padding:10px;border-bottom:1px solid #eee;">execution(* *(..))</td><td style="padding:10px;border-bottom:1px solid #eee;">匹配所有方法</td></tr>
<tr><td style="padding:10px;border-bottom:1px solid #eee;">execution(* com.example.service.*.*(..))</td><td style="padding:10px;border-bottom:1px solid #eee;">service包下所有方法</td></tr>
<tr><td style="padding:10px;border-bottom:1px solid #eee;">execution(* com.example.service..*.*(..))</td><td style="padding:10px;border-bottom:1px solid #eee;">service包及子包所有方法</td></tr>
<tr><td style="padding:10px;border-bottom:1px solid #eee;">execution(public * *(..))</td><td style="padding:10px;border-bottom:1px solid #eee;">所有public方法</td></tr>
<tr><td style="padding:10px;border-bottom:1px solid #eee;">@annotation(org.springframework.transaction.annotation.Transactional)</td><td style="padding:10px;border-bottom:1px solid #eee;">带@Transactional的方法</td></tr>
</table>
</div>
</div>
<script>
async function testAop() {
const result = document.getElementById('testResult');
result.textContent = '测试中...';
try {
const res = await fetch('/api/learning/aop/test?message=HelloAOP');
const data = await res.json();
result.innerHTML = `<strong>✅ 测试成功</strong>\n\n${JSON.stringify(data, null, 2)}\n\n💡 提示: 查看服务器控制台,观察 AOP 通知执行顺序`;
} catch (e) {
result.textContent = '测试失败: ' + e.message;
}
}
async function testAopError() {
const result = document.getElementById('testResult');
result.textContent = '测试中...';
try {
const res = await fetch('/api/learning/aop/test-error?error=true');
const data = await res.json();
result.innerHTML = `<strong>✅ 正常返回</strong>\n\n${JSON.stringify(data, null, 2)}`;
} catch (e) {
result.innerHTML = `<strong>❌ 触发异常(预期行为)</strong>\n\n异常信息: ${e.message}\n\n💡 提示: 查看服务器控制台,观察 @AfterThrowing 通知`;
}
}
</script>
</body>
</html>

View File

@@ -0,0 +1,280 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>API 测试面板 - Spring Boot</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; }
.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 h1 { font-size: 2em; }
.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: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 h3 { color: #667eea; margin-bottom: 15px; border-bottom: 2px solid #eee; padding-bottom: 10px; }
.api-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); gap: 15px; }
.api-item { background: #f8f9fa; border-radius: 8px; padding: 15px; border-left: 4px solid; }
.api-item.get { border-color: #28a745; }
.api-item.post { border-color: #007bff; }
.api-item.put { border-color: #ffc107; }
.api-item.delete { border-color: #dc3545; }
.method { display: inline-block; padding: 3px 8px; border-radius: 4px; font-size: 0.75em; font-weight: bold; margin-right: 8px; }
.method.get { background: #28a745; color: white; }
.method.post { background: #007bff; color: white; }
.method.put { background: #ffc107; color: #333; }
.method.delete { background: #dc3545; color: white; }
.btn { padding: 8px 16px; border: none; border-radius: 5px; cursor: pointer; font-size: 0.9em; margin-top: 10px; }
.btn-primary { background: #667eea; color: white; }
.btn-primary:hover { background: #5a6fd6; }
.result { background: #1e1e1e; color: #d4d4d4; padding: 15px; border-radius: 5px; margin-top: 10px; font-family: monospace; font-size: 0.85em; overflow-x: auto; max-height: 200px; overflow-y: auto; display: none; }
.result.show { display: block; }
.url { font-family: monospace; color: #666; font-size: 0.9em; word-break: break-all; }
.tabs { display: flex; gap: 5px; margin-bottom: 20px; border-bottom: 2px solid #eee; }
.tab { padding: 10px 20px; cursor: pointer; border-bottom: 2px solid transparent; }
.tab.active { border-bottom-color: #667eea; color: #667eea; font-weight: 600; }
.tab-content { display: none; }
.tab-content.active { display: block; }
.json-input { width: 100%; min-height: 100px; padding: 10px; border: 1px solid #ddd; border-radius: 5px; font-family: monospace; font-size: 0.9em; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🔌 API 测试面板</h1>
<p>在线测试所有 RESTful API</p>
</div>
<div class="nav">
<a href="index.html">🏠 首页</a>
<a href="ioc.html">📦 IoC</a>
<a href="aop.html">🔪 AOP</a>
<a href="mybatis.html">💾 MyBatis</a>
<a href="transaction.html">🔄 事务</a>
<a href="users.html">👥 用户</a>
<a href="api.html" class="active">🔌 API</a>
</div>
<div class="tabs">
<div class="tab active" onclick="switchTab('user')">👥 用户 API</div>
<div class="tab" onclick="switchTab('product')">📦 产品 API</div>
<div class="tab" onclick="switchTab('order')">🛒 订单 API</div>
<div class="tab" onclick="switchTab('learning')">📚 学习 API</div>
</div>
<!-- 用户 API -->
<div id="userTab" class="tab-content active">
<div class="card">
<h3>用户管理 API</h3>
<div class="api-grid">
<div class="api-item get">
<span class="method get">GET</span>
<strong>获取所有用户</strong>
<div class="url">/api/users</div>
<button class="btn btn-primary" onclick="testApi('GET', '/api/users', null, 'userResult1')">测试</button>
<div id="userResult1" class="result"></div>
</div>
<div class="api-item get">
<span class="method get">GET</span>
<strong>获取单个用户</strong>
<div class="url">/api/users/{id}</div>
<input type="number" id="userId" placeholder="用户ID" style="width:80px;padding:5px;margin-top:5px;">
<button class="btn btn-primary" onclick="testApi('GET', '/api/users/' + document.getElementById('userId').value, null, 'userResult2')">测试</button>
<div id="userResult2" class="result"></div>
</div>
<div class="api-item post">
<span class="method post">POST</span>
<strong>创建用户</strong>
<div class="url">/api/users</div>
<textarea class="json-input" id="createUserJson">{
"username": "testuser",
"email": "test@example.com",
"phone": "13800138000",
"bio": "测试用户"
}</textarea>
<button class="btn btn-primary" onclick="testApi('POST', '/api/users', document.getElementById('createUserJson').value, 'userResult3')">测试</button>
<div id="userResult3" class="result"></div>
</div>
<div class="api-item get">
<span class="method get">GET</span>
<strong>搜索用户</strong>
<div class="url">/api/users/search?username={name}</div>
<input type="text" id="searchName" placeholder="用户名关键词" style="padding:5px;margin-top:5px;">
<button class="btn btn-primary" onclick="testApi('GET', '/api/users/search?username=' + encodeURIComponent(document.getElementById('searchName').value), null, 'userResult4')">测试</button>
<div id="userResult4" class="result"></div>
</div>
</div>
</div>
</div>
<!-- 产品 API -->
<div id="productTab" class="tab-content">
<div class="card">
<h3>产品管理 API</h3>
<div class="api-grid">
<div class="api-item get">
<span class="method get">GET</span>
<strong>获取所有产品</strong>
<div class="url">/api/products</div>
<button class="btn btn-primary" onclick="testApi('GET', '/api/products', null, 'productResult1')">测试</button>
<div id="productResult1" class="result"></div>
</div>
<div class="api-item post">
<span class="method post">POST</span>
<strong>创建产品</strong>
<div class="url">/api/products</div>
<textarea class="json-input" id="createProductJson">{
"name": "iPhone 15",
"description": "最新款苹果手机",
"price": 5999.00,
"stockQuantity": 100,
"category": "手机"
}</textarea>
<button class="btn btn-primary" onclick="testApi('POST', '/api/products', document.getElementById('createProductJson').value, 'productResult2')">测试</button>
<div id="productResult2" class="result"></div>
</div>
</div>
</div>
</div>
<!-- 订单 API -->
<div id="orderTab" class="tab-content">
<div class="card">
<h3>订单管理 API (演示事务)</h3>
<div class="api-grid">
<div class="api-item get">
<span class="method get">GET</span>
<strong>获取所有订单</strong>
<div class="url">/api/orders</div>
<button class="btn btn-primary" onclick="testApi('GET', '/api/orders', null, 'orderResult1')">测试</button>
<div id="orderResult1" class="result"></div>
</div>
<div class="api-item post">
<span class="method post">POST</span>
<strong>创建订单</strong>
<div class="url">/api/orders</div>
<textarea class="json-input" id="createOrderJson">{
"userId": 1,
"productId": 1,
"quantity": 1,
"rollback": false
}</textarea>
<button class="btn btn-primary" onclick="testApi('POST', '/api/orders', document.getElementById('createOrderJson').value, 'orderResult2')">测试</button>
<div id="orderResult2" class="result"></div>
</div>
<div class="api-item post">
<span class="method post">POST</span>
<strong>创建订单(触发回滚)</strong>
<div class="url">/api/orders (rollback=true)</div>
<textarea class="json-input" id="rollbackOrderJson">{
"userId": 1,
"productId": 1,
"quantity": 1,
"rollback": true
}</textarea>
<button class="btn btn-primary" onclick="testApi('POST', '/api/orders', document.getElementById('rollbackOrderJson').value, 'orderResult3')">测试</button>
<div id="orderResult3" class="result"></div>
</div>
</div>
</div>
</div>
<!-- 学习 API -->
<div id="learningTab" class="tab-content">
<div class="card">
<h3>学习 API</h3>
<div class="api-grid">
<div class="api-item get">
<span class="method get">GET</span>
<strong>IoC - 查看所有 Bean</strong>
<div class="url">/api/learning/ioc/beans</div>
<button class="btn btn-primary" onclick="testApi('GET', '/api/learning/ioc/beans', null, 'learnResult1')">测试</button>
<div id="learnResult1" class="result"></div>
</div>
<div class="api-item get">
<span class="method get">GET</span>
<strong>AOP - 概念</strong>
<div class="url">/api/learning/aop/concepts</div>
<button class="btn btn-primary" onclick="testApi('GET', '/api/learning/aop/concepts', null, 'learnResult2')">测试</button>
<div id="learnResult2" class="result"></div>
</div>
<div class="api-item get">
<span class="method get">GET</span>
<strong>MyBatis - 配置</strong>
<div class="url">/api/learning/mybatis/config</div>
<button class="btn btn-primary" onclick="testApi('GET', '/api/learning/mybatis/config', null, 'learnResult3')">测试</button>
<div id="learnResult3" class="result"></div>
</div>
<div class="api-item get">
<span class="method get">GET</span>
<strong>事务 - 传播行为</strong>
<div class="url">/api/learning/transaction/propagation</div>
<button class="btn btn-primary" onclick="testApi('GET', '/api/learning/transaction/propagation', null, 'learnResult4')">测试</button>
<div id="learnResult4" class="result"></div>
</div>
</div>
</div>
</div>
</div>
<script>
function switchTab(tab) {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
event.target.classList.add('active');
document.getElementById(tab + 'Tab').classList.add('active');
}
async function testApi(method, url, body, resultId) {
const resultDiv = document.getElementById(resultId);
resultDiv.classList.add('show');
resultDiv.textContent = '请求中...';
try {
const options = {
method: method,
headers: {}
};
if (body) {
options.headers['Content-Type'] = 'application/json';
options.body = body;
}
const startTime = Date.now();
const res = await fetch(url, options);
const duration = Date.now() - startTime;
const data = await res.json();
resultDiv.innerHTML = `<strong>${res.status} ${res.statusText}</strong> (${duration}ms)\n\n${JSON.stringify(data, null, 2)}`;
} catch (e) {
resultDiv.innerHTML = `<strong style="color:#ff6b6b;">Error</strong>\n\n${e.message}`;
}
}
</script>
</body>
</html>

View File

@@ -0,0 +1,203 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Spring Boot 学习中心</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; }
.container { max-width: 1400px; margin: 0 auto; padding: 20px; }
.header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 40px 20px; text-align: center; margin-bottom: 30px; border-radius: 10px; }
.header h1 { font-size: 2.5em; margin-bottom: 10px; }
.header p { opacity: 0.9; font-size: 1.1em; }
.nav { display: flex; gap: 10px; margin-bottom: 30px; flex-wrap: wrap; justify-content: center; }
.nav a { padding: 12px 24px; background: white; border-radius: 25px; text-decoration: none; color: #333; font-weight: 500; transition: all 0.3s; border: 2px solid #e0e0e0; }
.nav a:hover, .nav a.active { background: #667eea; color: white; border-color: #667eea; }
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; }
.card { background: white; border-radius: 15px; padding: 25px; box-shadow: 0 5px 20px rgba(0,0,0,0.08); transition: transform 0.3s; }
.card:hover { transform: translateY(-5px); }
.card h3 { color: #333; margin-bottom: 15px; font-size: 1.3em; }
.card p { color: #666; line-height: 1.6; margin-bottom: 15px; }
.card .btn { display: inline-block; padding: 10px 20px; background: #667eea; color: white; text-decoration: none; border-radius: 20px; font-size: 0.9em; }
.card .btn:hover { background: #5a6fd6; }
.feature-list { list-style: none; }
.feature-list li { padding: 8px 0; border-bottom: 1px solid #eee; }
.feature-list li:last-child { border-bottom: none; }
.feature-list .icon { margin-right: 8px; }
.api-test { background: #1e1e1e; border-radius: 10px; padding: 20px; margin-top: 20px; }
.api-test h4 { color: #4ec9b0; margin-bottom: 15px; }
.api-test pre { color: #d4d4d4; overflow-x: auto; font-size: 0.85em; }
.api-test .method { color: #569cd6; }
.api-test .url { color: #4ec9b0; }
.status { display: flex; gap: 20px; flex-wrap: wrap; margin-bottom: 30px; }
.status-item { background: white; padding: 20px 30px; border-radius: 10px; text-align: center; }
.status-item .value { font-size: 2em; font-weight: bold; color: #667eea; }
.status-item .label { color: #666; margin-top: 5px; }
footer { text-align: center; padding: 30px; color: #666; margin-top: 40px; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🍃 Spring Boot 学习中心</h1>
<p>交互式学习 Spring 核心功能 | IoC · AOP · MyBatis · 事务</p>
</div>
<div class="status">
<div class="status-item">
<div class="value" id="beanCount">-</div>
<div class="label">已加载 Bean</div>
</div>
<div class="status-item">
<div class="value" id="userCount">-</div>
<div class="label">用户数量</div>
</div>
<div class="status-item">
<div class="value" id="productCount">-</div>
<div class="label">产品数量</div>
</div>
<div class="status-item">
<div class="value" id="orderCount">-</div>
<div class="label">订单数量</div>
</div>
</div>
<div class="nav">
<a href="index.html" class="active">🏠 首页</a>
<a href="ioc.html">📦 IoC 容器</a>
<a href="aop.html">🔪 AOP 切面</a>
<a href="mybatis.html">💾 MyBatis</a>
<a href="transaction.html">🔄 事务管理</a>
<a href="users.html">👥 用户管理</a>
<a href="api.html">🔌 API 测试</a>
</div>
<div class="grid">
<div class="card">
<h3>📦 IoC 容器</h3>
<p>理解 Spring 的核心:控制反转和依赖注入。学习 Bean 的生命周期、作用域和各种注入方式。</p>
<ul class="feature-list">
<li><span class="icon"></span>Bean 生命周期演示</li>
<li><span class="icon"></span>依赖注入方式对比</li>
<li><span class="icon"></span>Bean 作用域详解</li>
<li><span class="icon"></span>性能统计面板</li>
</ul>
<a href="ioc.html" class="btn">开始学习 →</a>
</div>
<div class="card">
<h3>🔪 AOP 切面编程</h3>
<p>掌握面向切面编程,实现日志、性能监控、事务等横切关注点的模块化管理。</p>
<ul class="feature-list">
<li><span class="icon"></span>5 种通知类型演示</li>
<li><span class="icon"></span>切入点表达式语法</li>
<li><span class="icon"></span>实时日志展示</li>
<li><span class="icon"></span>性能监控切面</li>
</ul>
<a href="aop.html" class="btn">开始学习 →</a>
</div>
<div class="card">
<h3>💾 MyBatis 集成</h3>
<p>学习 MyBatis 与 Spring Boot 的整合,对比 JPA掌握动态 SQL 和缓存机制。</p>
<ul class="feature-list">
<li><span class="icon"></span>MyBatis vs JPA 对比</li>
<li><span class="icon"></span>动态 SQL 语法</li>
<li><span class="icon"></span>一级/二级缓存演示</li>
<li><span class="icon"></span>批量操作示例</li>
</ul>
<a href="mybatis.html" class="btn">开始学习 →</a>
</div>
<div class="card">
<h3>🔄 事务管理</h3>
<p>深入理解 Spring 声明式事务,掌握传播行为和隔离级别的实际应用。</p>
<ul class="feature-list">
<li><span class="icon"></span>事务传播行为</li>
<li><span class="icon"></span>事务隔离级别</li>
<li><span class="icon"></span>回滚机制演示</li>
<li><span class="icon"></span>订单创建场景</li>
</ul>
<a href="transaction.html" class="btn">开始学习 →</a>
</div>
<div class="card">
<h3>👥 用户管理 CRUD</h3>
<p>完整的 RESTful API 示例,演示增删改查操作和参数验证。</p>
<ul class="feature-list">
<li><span class="icon"></span>RESTful 设计规范</li>
<li><span class="icon"></span>参数验证</li>
<li><span class="icon"></span>异常处理</li>
<li><span class="icon"></span>交互式测试</li>
</ul>
<a href="users.html" class="btn">开始学习 →</a>
</div>
<div class="card">
<h3>🔌 API 测试面板</h3>
<p>在线测试所有 API 接口,查看请求响应,理解 RESTful API 工作原理。</p>
<ul class="feature-list">
<li><span class="icon"></span>用户 API</li>
<li><span class="icon"></span>产品 API</li>
<li><span class="icon"></span>订单 API</li>
<li><span class="icon"></span>学习 API</li>
</ul>
<a href="api.html" class="btn">开始测试 →</a>
</div>
</div>
<div class="api-test">
<h4>🚀 快速开始 - API 示例</h4>
<pre>
<span class="method">GET</span> <span class="url">/api/users</span> # 获取所有用户
<span class="method">GET</span> <span class="url">/api/users/{id}</span> # 获取单个用户
<span class="method">POST</span> <span class="url">/api/users</span> # 创建用户
<span class="method">PUT</span> <span class="url">/api/users/{id}</span> # 更新用户
<span class="method">DEL</span> <span class="url">/api/users/{id}</span> # 删除用户
<span class="method">GET</span> <span class="url">/api/learning/ioc/beans</span> # 查看所有 Bean
<span class="method">GET</span> <span class="url">/api/learning/aop/concepts</span> # AOP 概念
<span class="method">GET</span> <span class="url">/api/learning/mybatis/cache</span> # 缓存机制
<span class="method">GET</span> <span class="url">/api/learning/transaction/propagation</span> # 传播行为
</pre>
</div>
<footer>
<p>🍃 Spring Boot 学习脚手架 | <a href="/h2-console" target="_blank">H2 控制台</a> (JDBC: jdbc:h2:file:~/h2/springboot_scaffold, 用户: sa)</p>
</footer>
</div>
<script>
// 加载状态数据
async function loadStatus() {
try {
const [beans, users, products, orders] = await Promise.all([
fetch('/api/learning/ioc/beans').then(r => r.json()),
fetch('/api/users/count').then(r => r.json()),
fetch('/api/products').then(r => r.json()),
fetch('/api/orders').then(r => r.json())
]);
document.getElementById('beanCount').textContent = beans.total || '-';
document.getElementById('userCount').textContent = users.count || 0;
document.getElementById('productCount').textContent = products.length || 0;
document.getElementById('orderCount').textContent = orders.length || 0;
} catch (e) {
console.error('加载状态失败:', e);
}
}
loadStatus();
setInterval(loadStatus, 30000);
</script>
</body>
</html>

View File

@@ -0,0 +1,175 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>IoC 容器学习 - Spring Boot</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; }
.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 h1 { font-size: 2em; }
.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: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 h3 { color: #667eea; margin-bottom: 15px; border-bottom: 2px solid #eee; padding-bottom: 10px; }
.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; }
.btn { padding: 10px 20px; background: #667eea; color: white; border: none; border-radius: 5px; cursor: pointer; margin: 5px; }
.btn:hover { background: #5a6fd6; }
.btn-secondary { background: #6c757d; }
.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; }
table { width: 100%; border-collapse: collapse; margin: 10px 0; }
th, td { padding: 10px; text-align: left; border-bottom: 1px solid #eee; }
th { background: #f8f9fa; font-weight: 600; }
tr:hover { background: #f8f9fa; }
.badge { padding: 4px 8px; border-radius: 4px; font-size: 0.8em; }
.badge-primary { background: #667eea; color: white; }
.badge-success { background: #28a745; color: white; }
.badge-warning { background: #ffc107; color: #333; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>📦 IoC 容器学习</h1>
<p>控制反转 (Inversion of Control) 与依赖注入 (Dependency Injection)</p>
</div>
<div class="nav">
<a href="index.html">🏠 首页</a>
<a href="ioc.html" class="active">📦 IoC</a>
<a href="aop.html">🔪 AOP</a>
<a href="mybatis.html">💾 MyBatis</a>
<a href="transaction.html">🔄 事务</a>
<a href="users.html">👥 用户</a>
<a href="api.html">🔌 API</a>
</div>
<div class="card">
<h3>📚 核心概念</h3>
<div class="concept-box">
<h4>什么是 IoC</h4>
<p><strong>控制反转 (Inversion of Control)</strong>:将对象的创建和管理交给 Spring 容器,而不是由开发者手动创建。</p>
<p><strong>依赖注入 (Dependency Injection)</strong>IoC 的一种实现方式通过构造器、Setter 或字段将依赖注入到对象中。</p>
</div>
<div class="concept-box">
<h4>为什么用 IoC</h4>
<ul>
<li><strong>解耦</strong>:对象之间不直接依赖,通过接口交互</li>
<li><strong>可测试</strong>:方便使用 Mock 对象进行单元测试</li>
<li><strong>可维护</strong>:集中管理对象生命周期</li>
<li><strong>AOP 支持</strong>:便于实现切面编程</li>
</ul>
</div>
</div>
<div class="card">
<h3>🔍 查看所有 Bean</h3>
<p>Spring 容器中管理的所有 Bean 对象</p>
<button class="btn" onclick="loadBeans()">刷新 Bean 列表</button>
<button class="btn btn-secondary" onclick="document.getElementById('beansResult').innerHTML=''">清空</button>
<div id="beansResult" class="result"></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>
<script>
async function loadBeans() {
const result = document.getElementById('beansResult');
result.textContent = '加载中...';
try {
const res = await fetch('/api/learning/ioc/beans');
const data = await res.json();
result.innerHTML = `<strong>总 Bean 数: ${data.total}</strong>\n\n用户相关 Bean:\n${data.userBeans.map(b => ' - ' + b).join('\n')}`;
} catch (e) {
result.textContent = '加载失败: ' + e.message;
}
}
async function testScopes() {
const result = document.getElementById('scopesResult');
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>
</body>
</html>

View File

@@ -0,0 +1,176 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MyBatis 学习 - Spring Boot</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; }
.container { max-width: 1400px; margin: 0 auto; padding: 20px; }
.header { background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); color: white; padding: 30px 20px; text-align: center; margin-bottom: 20px; border-radius: 10px; }
.header h1 { font-size: 2em; }
.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:hover, .nav a.active { background: #4facfe; 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 h3 { color: #4facfe; margin-bottom: 15px; border-bottom: 2px solid #eee; padding-bottom: 10px; }
.comparison { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
.compare-box { padding: 20px; border-radius: 10px; }
.mybatis-box { background: #e3f2fd; border: 2px solid #4facfe; }
.jpa-box { background: #f3e5f5; border: 2px solid #9c27b0; }
.btn { padding: 10px 20px; background: #4facfe; color: white; border: none; border-radius: 5px; cursor: pointer; margin: 5px; }
.btn:hover { background: #3d9be8; }
.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; }
.code-block { background: #f4f4f4; padding: 15px; border-radius: 5px; font-family: monospace; font-size: 0.9em; overflow-x: auto; margin: 10px 0; }
.cache-box { background: #fff3e0; border-left: 4px solid #ff9800; padding: 15px; margin: 10px 0; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>💾 MyBatis 学习</h1>
<p>半自动 ORM 框架 - SQL 与 Java 对象的映射</p>
</div>
<div class="nav">
<a href="index.html">🏠 首页</a>
<a href="ioc.html">📦 IoC</a>
<a href="aop.html">🔪 AOP</a>
<a href="mybatis.html" class="active">💾 MyBatis</a>
<a href="transaction.html">🔄 事务</a>
<a href="users.html">👥 用户</a>
<a href="api.html">🔌 API</a>
</div>
<div class="card">
<h3>📊 MyBatis vs JPA 对比</h3>
<div class="comparison">
<div class="compare-box mybatis-box">
<h4>MyBatis</h4>
<p><strong>优点:</strong></p>
<ul>
<li>✅ SQL 灵活可控</li>
<li>✅ 性能优化方便</li>
<li>✅ 复杂查询友好</li>
<li>✅ 易于 DBA 协作</li>
</ul>
<p><strong>缺点:</strong></p>
<ul>
<li>❌ SQL 与代码耦合</li>
<li>❌ 数据库迁移成本高</li>
<li>❌ 需要手写 SQL</li>
</ul>
<p><strong>适用:</strong>复杂查询、性能要求高</p>
</div>
<div class="compare-box jpa-box">
<h4>JPA/Hibernate</h4>
<p><strong>优点:</strong></p>
<ul>
<li>✅ 面向对象</li>
<li>✅ 数据库无关</li>
<li>✅ 开发效率高</li>
<li>✅ 自动维护</li>
</ul>
<p><strong>缺点:</strong></p>
<ul>
<li>❌ 复杂查询困难</li>
<li>❌ 性能调优复杂</li>
<li>❌ 学习曲线陡</li>
</ul>
<p><strong>适用:</strong>标准 CRUD、快速开发</p>
</div>
</div>
</div>
<div class="card">
<h3>🚀 快速体验</h3>
<button class="btn" onclick="loadConfig()">查看 MyBatis 配置</button>
<button class="btn" onclick="loadConcepts()">核心概念</button>
<button class="btn" onclick="loadVsJpa()">详细对比</button>
<div id="result" class="result"></div>
</div>
<div class="card">
<h3>💡 缓存机制</h3>
<div class="cache-box">
<h4>一级缓存 (SqlSession 级别)</h4>
<p>默认开启,同一 SqlSession 内相同查询只执行一次 SQL</p>
<div class="code-block">// 第一次查询 - 执行 SQL<br>User u1 = mapper.findById(1L);<br>// 第二次查询 - 命中缓存,不执行 SQL<br>User u2 = mapper.findById(1L);</div>
</div>
<div class="cache-box">
<h4>二级缓存 (Mapper 级别)</h4>
<p>需配置 @CacheNamespace多个 SqlSession 共享</p>
<div class="code-block">@Mapper<br>@CacheNamespace<br>public interface UserMapper { ... }</div>
</div>
<p>💡 <strong>验证方式:</strong>查看控制台 SQL 日志,缓存命中时不打印 SQL</p>
</div>
<div class="card">
<h3>📝 常用注解</h3>
<table style="width:100%;border-collapse:collapse;">
<tr style="background:#f8f9fa;"><th style="padding:10px;text-align:left;">注解</th><th style="padding:10px;text-align:left;">用途</th></tr>
<tr><td style="padding:10px;border-bottom:1px solid #eee;">@Mapper</td><td style="padding:10px;border-bottom:1px solid #eee;">标记为 MyBatis Mapper 接口</td></tr>
<tr><td style="padding:10px;border-bottom:1px solid #eee;">@Select</td><td style="padding:10px;border-bottom:1px solid #eee;">查询 SQL</td></tr>
<tr><td style="padding:10px;border-bottom:1px solid #eee;">@Insert</td><td style="padding:10px;border-bottom:1px solid #eee;">插入 SQL</td></tr>
<tr><td style="padding:10px;border-bottom:1px solid #eee;">@Update</td><td style="padding:10px;border-bottom:1px solid #eee;">更新 SQL</td></tr>
<tr><td style="padding:10px;border-bottom:1px solid #eee;">@Delete</td><td style="padding:10px;border-bottom:1px solid #eee;">删除 SQL</td></tr>
<tr><td style="padding:10px;border-bottom:1px solid #eee;">@Param</td><td style="padding:10px;border-bottom:1px solid #eee;">参数命名</td></tr>
<tr><td style="padding:10px;border-bottom:1px solid #eee;">@Options</td><td style="padding:10px;border-bottom:1px solid #eee;">额外选项如返回自增ID</td></tr>
<tr><td style="padding:10px;border-bottom:1px solid #eee;">@CacheNamespace</td><td style="padding:10px;border-bottom:1px solid #eee;">启用二级缓存</td></tr>
</table>
</div>
<div class="card">
<h3>🔧 动态 SQL 示例</h3>
<div class="code-block">&lt;select id="findUsers"&gt;<br> SELECT * FROM users<br> &lt;where&gt;<br> &lt;if test="name != null"&gt;<br> AND name LIKE CONCAT('%', #{name}, '%')<br> &lt;/if&gt;<br> &lt;if test="email != null"&gt;<br> AND email = #{email}<br> &lt;/if&gt;<br> &lt;/where&gt;<br>&lt;/select&gt;</div>
</div>
</div>
<script>
async function loadConfig() {
const result = document.getElementById('result');
result.textContent = '加载中...';
try {
const res = await fetch('/api/learning/mybatis/config');
const data = await res.json();
result.innerHTML = `<strong>MyBatis 配置</strong>\n\n${JSON.stringify(data, null, 2)}`;
} catch (e) {
result.textContent = '加载失败: ' + e.message;
}
}
async function loadConcepts() {
const result = document.getElementById('result');
result.textContent = '加载中...';
try {
const res = await fetch('/api/learning/mybatis/concepts');
const data = await res.json();
result.innerHTML = `<strong>核心概念</strong>\n\n${JSON.stringify(data, null, 2)}`;
} catch (e) {
result.textContent = '加载失败: ' + e.message;
}
}
async function loadVsJpa() {
const result = document.getElementById('result');
result.textContent = '加载中...';
try {
const res = await fetch('/api/learning/mybatis/vs-jpa');
const data = await res.json();
result.innerHTML = `<strong>MyBatis vs JPA</strong>\n\n${JSON.stringify(data, null, 2)}`;
} catch (e) {
result.textContent = '加载失败: ' + e.message;
}
}
</script>
</body>
</html>

View File

@@ -0,0 +1,191 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>事务管理学习 - Spring Boot</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; }
.container { max-width: 1400px; margin: 0 auto; padding: 20px; }
.header { background: linear-gradient(135deg, #fa709a 0%, #fee140 100%); color: white; padding: 30px 20px; text-align: center; margin-bottom: 20px; border-radius: 10px; }
.header h1 { font-size: 2em; }
.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:hover, .nav a.active { background: #fa709a; 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 h3 { color: #fa709a; margin-bottom: 15px; border-bottom: 2px solid #eee; padding-bottom: 10px; }
.acid-box { display: grid; grid-template-columns: repeat(4, 1fr); gap: 15px; margin: 20px 0; }
.acid-item { background: #f8f9fa; padding: 20px; border-radius: 10px; text-align: center; border-top: 4px solid; }
.acid-a { border-color: #e74c3c; }
.acid-c { border-color: #3498db; }
.acid-i { border-color: #2ecc71; }
.acid-d { border-color: #9b59b6; }
.acid-item h4 { font-size: 2em; margin-bottom: 10px; }
.btn { padding: 10px 20px; background: #fa709a; color: white; border: none; border-radius: 5px; cursor: pointer; margin: 5px; }
.btn:hover { background: #e85a8a; }
.btn-danger { background: #e74c3c; }
.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; }
.propagation-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 15px; }
.prop-box { background: #f8f9fa; padding: 15px; border-radius: 8px; border-left: 4px solid #fa709a; }
.prop-box h4 { color: #fa709a; margin-bottom: 8px; }
table { width: 100%; border-collapse: collapse; margin: 15px 0; }
th, td { padding: 12px; text-align: left; border-bottom: 1px solid #eee; }
th { background: #f8f9fa; font-weight: 600; }
tr:hover { background: #f8f9fa; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🔄 事务管理</h1>
<p>声明式事务 - @Transactional</p>
</div>
<div class="nav">
<a href="index.html">🏠 首页</a>
<a href="ioc.html">📦 IoC</a>
<a href="aop.html">🔪 AOP</a>
<a href="mybatis.html">💾 MyBatis</a>
<a href="transaction.html" class="active">🔄 事务</a>
<a href="users.html">👥 用户</a>
<a href="api.html">🔌 API</a>
</div>
<div class="card">
<h3>📚 ACID 特性</h3>
<div class="acid-box">
<div class="acid-item acid-a">
<h4>A</h4>
<p><strong>Atomicity</strong></p>
<p>原子性</p>
<p style="font-size:0.9em;color:#666;">事务是不可分割的工作单位</p>
</div>
<div class="acid-item acid-c">
<h4>C</h4>
<p><strong>Consistency</strong></p>
<p>一致性</p>
<p style="font-size:0.9em;color:#666;">数据库状态保持一致</p>
</div>
<div class="acid-item acid-i">
<h4>I</h4>
<p><strong>Isolation</strong></p>
<p>隔离性</p>
<p style="font-size:0.9em;color:#666;">事务之间相互隔离</p>
</div>
<div class="acid-item acid-d">
<h4>D</h4>
<p><strong>Durability</strong></p>
<p>持久性</p>
<p style="font-size:0.9em;color:#666;">提交后永久保存</p>
</div>
</div>
</div>
<div class="card">
<h3>🚀 传播行为 (Propagation)</h3>
<div class="propagation-grid">
<div class="prop-box">
<h4>REQUIRED (默认)</h4>
<p>有事务则加入,无则新建</p>
<p style="font-size:0.85em;color:#666;">最常用,适合大多数业务方法</p>
</div>
<div class="prop-box">
<h4>REQUIRES_NEW</h4>
<p>总是新建事务,挂起当前事务</p>
<p style="font-size:0.85em;color:#666;">适合日志记录、独立子任务</p>
</div>
<div class="prop-box">
<h4>SUPPORTS</h4>
<p>有事务则加入,无则以非事务运行</p>
<p style="font-size:0.85em;color:#666;">适合查询方法</p>
</div>
<div class="prop-box">
<h4>NOT_SUPPORTED</h4>
<p>以非事务运行,挂起当前事务</p>
<p style="font-size:0.85em;color:#666;">不需要事务的操作</p>
</div>
<div class="prop-box">
<h4>MANDATORY</h4>
<p>必须在事务中运行,否则抛异常</p>
<p style="font-size:0.85em;color:#666;">强制要求事务</p>
</div>
<div class="prop-box">
<h4>NEVER</h4>
<p>不能在事务中运行,否则抛异常</p>
<p style="font-size:0.85em;color:#666;">确保无事务</p>
</div>
</div>
<button class="btn" onclick="loadPropagation()">查看详细说明</button>
<div id="propResult" class="result"></div>
</div>
<div class="card">
<h3>🔒 隔离级别 (Isolation)</h3>
<table>
<tr><th>级别</th><th>脏读</th><th>不可重复读</th><th>幻读</th><th>说明</th></tr>
<tr><td>READ_UNCOMMITTED</td><td></td><td></td><td></td><td>读未提交,性能最高</td></tr>
<tr><td>READ_COMMITTED</td><td></td><td></td><td></td><td>读已提交Oracle默认</td></tr>
<tr><td>REPEATABLE_READ</td><td></td><td></td><td></td><td>可重复读MySQL默认</td></tr>
<tr><td>SERIALIZABLE</td><td></td><td></td><td></td><td>串行化,性能最低</td></tr>
</table>
<p style="margin-top:10px;color:#666;">✅ = 防止该问题 | ❌ = 可能出现该问题</p>
<button class="btn" onclick="loadIsolation()">查看详细说明</button>
<div id="isoResult" class="result"></div>
</div>
<div class="card">
<h3>🧪 事务回滚演示</h3>
<p>创建订单时触发异常,观察事务回滚效果</p>
<button class="btn" onclick="loadRollback()">查看回滚规则</button>
<div id="rollbackResult" class="result"></div>
</div>
</div>
<script>
async function loadPropagation() {
const result = document.getElementById('propResult');
result.textContent = '加载中...';
try {
const res = await fetch('/api/learning/transaction/propagation');
const data = await res.json();
result.innerHTML = `<strong>传播行为详解</strong>\n\n${JSON.stringify(data, null, 2)}`;
} catch (e) {
result.textContent = '加载失败: ' + e.message;
}
}
async function loadIsolation() {
const result = document.getElementById('isoResult');
result.textContent = '加载中...';
try {
const res = await fetch('/api/learning/transaction/isolation');
const data = await res.json();
result.innerHTML = `<strong>隔离级别详解</strong>\n\n${JSON.stringify(data, null, 2)}`;
} catch (e) {
result.textContent = '加载失败: ' + e.message;
}
}
async function loadRollback() {
const result = document.getElementById('rollbackResult');
result.textContent = '加载中...';
try {
const res = await fetch('/api/learning/transaction/rollback');
const data = await res.json();
result.innerHTML = `<strong>回滚规则</strong>\n\n${JSON.stringify(data, null, 2)}`;
} catch (e) {
result.textContent = '加载失败: ' + e.message;
}
}
</script>
</body>
</html>

View File

@@ -0,0 +1,278 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>用户管理 - Spring Boot CRUD</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; }
.container { max-width: 1400px; margin: 0 auto; padding: 20px; }
.header { background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%); color: white; padding: 30px 20px; text-align: center; margin-bottom: 20px; border-radius: 10px; }
.header h1 { font-size: 2em; }
.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:hover, .nav a.active { background: #11998e; 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 h3 { color: #11998e; margin-bottom: 15px; border-bottom: 2px solid #eee; padding-bottom: 10px; }
.form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 15px; margin-bottom: 20px; }
.form-group { display: flex; flex-direction: column; }
.form-group label { margin-bottom: 5px; color: #666; font-size: 0.9em; }
.form-group input, .form-group textarea { padding: 10px; border: 1px solid #ddd; border-radius: 5px; font-size: 1em; }
.form-group input:focus, .form-group textarea:focus { outline: none; border-color: #11998e; }
.btn { padding: 10px 20px; border: none; border-radius: 5px; cursor: pointer; font-size: 1em; margin: 5px; }
.btn-primary { background: #11998e; color: white; }
.btn-primary:hover { background: #0d7a6e; }
.btn-secondary { background: #6c757d; color: white; }
.btn-danger { background: #dc3545; color: white; }
.btn-warning { background: #ffc107; color: #333; }
table { width: 100%; border-collapse: collapse; margin-top: 20px; }
th, td { padding: 12px; text-align: left; border-bottom: 1px solid #eee; }
th { background: #f8f9fa; font-weight: 600; }
tr:hover { background: #f8f9fa; }
.actions { display: flex; gap: 5px; }
.actions button { padding: 5px 10px; font-size: 0.85em; }
.badge { padding: 4px 8px; border-radius: 4px; font-size: 0.8em; }
.badge-active { background: #28a745; color: white; }
.badge-inactive { background: #6c757d; color: white; }
.search-box { display: flex; gap: 10px; margin-bottom: 20px; }
.search-box input { flex: 1; padding: 10px; border: 1px solid #ddd; border-radius: 5px; }
.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: 300px; overflow-y: auto; }
.result.success { background: #d4edda; color: #155724; }
.result.error { background: #f8d7da; color: #721c24; }
.modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 1000; }
.modal-content { background: white; margin: 50px auto; padding: 30px; width: 90%; max-width: 600px; border-radius: 10px; }
.modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
.modal-header h3 { margin: 0; border: none; }
.close { font-size: 1.5em; cursor: pointer; color: #999; }
.close:hover { color: #333; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>👥 用户管理</h1>
<p>RESTful CRUD 操作演示</p>
</div>
<div class="nav">
<a href="index.html">🏠 首页</a>
<a href="ioc.html">📦 IoC</a>
<a href="aop.html">🔪 AOP</a>
<a href="mybatis.html">💾 MyBatis</a>
<a href="transaction.html">🔄 事务</a>
<a href="users.html" class="active">👥 用户</a>
<a href="api.html">🔌 API</a>
</div>
<div class="card">
<h3> 创建/编辑用户</h3>
<form id="userForm">
<input type="hidden" id="userId">
<div class="form-grid">
<div class="form-group">
<label>用户名 *</label>
<input type="text" id="username" required placeholder="输入用户名">
</div>
<div class="form-group">
<label>邮箱 *</label>
<input type="email" id="email" required placeholder="输入邮箱">
</div>
<div class="form-group">
<label>手机号</label>
<input type="text" id="phone" placeholder="输入手机号">
</div>
<div class="form-group">
<label>状态</label>
<select id="active" style="padding:10px;border:1px solid #ddd;border-radius:5px;">
<option value="true">启用</option>
<option value="false">禁用</option>
</select>
</div>
</div>
<div class="form-group" style="margin-bottom:15px;">
<label>简介</label>
<textarea id="bio" rows="3" placeholder="输入个人简介"></textarea>
</div>
<button type="submit" class="btn btn-primary">💾 保存</button>
<button type="button" class="btn btn-secondary" onclick="resetForm()">🔄 重置</button>
</form>
<div id="formResult"></div>
</div>
<div class="card">
<h3>📋 用户列表</h3>
<div class="search-box">
<input type="text" id="searchInput" placeholder="搜索用户名...">
<button class="btn btn-primary" onclick="searchUsers()">🔍 搜索</button>
<button class="btn btn-secondary" onclick="loadUsers()">🔄 刷新</button>
</div>
<table>
<thead>
<tr>
<th>ID</th>
<th>用户名</th>
<th>邮箱</th>
<th>手机号</th>
<th>状态</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody id="userTable">
<tr><td colspan="7" style="text-align:center;color:#999;">加载中...</td></tr>
</tbody>
</table>
</div>
</div>
<script>
let users = [];
async function loadUsers() {
const tbody = document.getElementById('userTable');
tbody.innerHTML = '<tr><td colspan="7" style="text-align:center;color:#999;">加载中...</td></tr>';
try {
const res = await fetch('/api/users');
users = await res.json();
renderUsers(users);
} catch (e) {
tbody.innerHTML = `<tr><td colspan="7" style="text-align:center;color:#dc3545;">加载失败: ${e.message}</td></tr>`;
}
}
function renderUsers(data) {
const tbody = document.getElementById('userTable');
if (data.length === 0) {
tbody.innerHTML = '<tr><td colspan="7" style="text-align:center;color:#999;">暂无数据</td></tr>';
return;
}
tbody.innerHTML = data.map(u => `
<tr>
<td>${u.id}</td>
<td>${u.username}</td>
<td>${u.email}</td>
<td>${u.phone || '-'}</td>
<td><span class="badge ${u.active ? 'badge-active' : 'badge-inactive'}">${u.active ? '启用' : '禁用'}</span></td>
<td>${u.createdAt ? new Date(u.createdAt).toLocaleString() : '-'}</td>
<td class="actions">
<button class="btn btn-warning" onclick="editUser(${u.id})">✏️ 编辑</button>
<button class="btn btn-danger" onclick="deleteUser(${u.id})">🗑️ 删除</button>
</td>
</tr>
`).join('');
}
async function searchUsers() {
const keyword = document.getElementById('searchInput').value.trim();
if (!keyword) {
loadUsers();
return;
}
try {
const res = await fetch(`/api/users/search?username=${encodeURIComponent(keyword)}`);
const data = await res.json();
renderUsers(data);
} catch (e) {
alert('搜索失败: ' + e.message);
}
}
document.getElementById('userForm').addEventListener('submit', async (e) => {
e.preventDefault();
const userId = document.getElementById('userId').value;
const user = {
username: document.getElementById('username').value,
email: document.getElementById('email').value,
phone: document.getElementById('phone').value,
bio: document.getElementById('bio').value,
active: document.getElementById('active').value === 'true'
};
const resultDiv = document.getElementById('formResult');
resultDiv.className = 'result';
resultDiv.textContent = '保存中...';
try {
const url = userId ? `/api/users/${userId}` : '/api/users';
const method = userId ? 'PUT' : 'POST';
const res = await fetch(url, {
method: method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(user)
});
const data = await res.json();
if (res.ok) {
resultDiv.className = 'result success';
resultDiv.innerHTML = `<strong>✅ 保存成功!</strong><br>ID: ${data.id}, 用户名: ${data.username}`;
resetForm();
loadUsers();
} else {
resultDiv.className = 'result error';
resultDiv.innerHTML = `<strong>❌ 保存失败</strong><br>${JSON.stringify(data)}`;
}
} catch (e) {
resultDiv.className = 'result error';
resultDiv.innerHTML = `<strong>❌ 错误</strong><br>${e.message}`;
}
});
function editUser(id) {
const user = users.find(u => u.id === id);
if (!user) return;
document.getElementById('userId').value = user.id;
document.getElementById('username').value = user.username;
document.getElementById('email').value = user.email;
document.getElementById('phone').value = user.phone || '';
document.getElementById('bio').value = user.bio || '';
document.getElementById('active').value = user.active.toString();
document.getElementById('formResult').innerHTML = '<span style="color:#11998e;">✏️ 正在编辑用户 #' + id + '</span>';
window.scrollTo(0, 0);
}
async function deleteUser(id) {
if (!confirm(`确定要删除用户 #${id} 吗?`)) return;
try {
const res = await fetch(`/api/users/${id}`, { method: 'DELETE' });
if (res.ok) {
alert('✅ 删除成功');
loadUsers();
} else {
alert('❌ 删除失败');
}
} catch (e) {
alert('删除失败: ' + e.message);
}
}
function resetForm() {
document.getElementById('userForm').reset();
document.getElementById('userId').value = '';
document.getElementById('formResult').innerHTML = '';
}
// 初始化
loadUsers();
</script>
</body>
</html>

View File

@@ -0,0 +1,22 @@
spring.application.name=springboot-scaffold
server.port=8082
# H2 Database
spring.h2.console.enabled=true
spring.datasource.url=jdbc:h2:file:~/h2/springboot_scaffold
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
# JPA
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
# MyBatis
mybatis.configuration.map-underscore-to-camel-case=true
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
# Logging
logging.level.com.example.scaffold=DEBUG
logging.level.org.springframework.transaction.interceptor=TRACE

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,159 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AOP 切面学习 - Spring Boot</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; }
.container { max-width: 1400px; margin: 0 auto; padding: 20px; }
.header { background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); color: white; padding: 30px 20px; text-align: center; margin-bottom: 20px; border-radius: 10px; }
.header h1 { font-size: 2em; }
.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:hover, .nav a.active { background: #f5576c; 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 h3 { color: #f5576c; margin-bottom: 15px; border-bottom: 2px solid #eee; padding-bottom: 10px; }
.advice-box { background: #f8f9fa; border-radius: 8px; padding: 15px; margin: 10px 0; border-left: 4px solid; }
.advice-before { border-color: #28a745; }
.advice-after { border-color: #6c757d; }
.advice-returning { border-color: #17a2b8; }
.advice-throwing { border-color: #dc3545; }
.advice-around { border-color: #ffc107; }
.btn { padding: 10px 20px; background: #f5576c; color: white; border: none; border-radius: 5px; cursor: pointer; margin: 5px; }
.btn:hover { background: #e0465b; }
.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; }
.code-block { background: #f4f4f4; padding: 15px; border-radius: 5px; font-family: monospace; font-size: 0.9em; overflow-x: auto; margin: 10px 0; }
.flow-diagram { display: flex; align-items: center; justify-content: center; flex-wrap: wrap; gap: 10px; padding: 20px; background: #f8f9fa; border-radius: 10px; margin: 15px 0; }
.flow-step { padding: 10px 20px; background: white; border-radius: 20px; border: 2px solid #ddd; }
.flow-arrow { font-size: 1.5em; color: #999; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🔪 AOP 切面编程</h1>
<p>Aspect Oriented Programming - 面向切面编程</p>
</div>
<div class="nav">
<a href="index.html">🏠 首页</a>
<a href="ioc.html">📦 IoC</a>
<a href="aop.html" class="active">🔪 AOP</a>
<a href="mybatis.html">💾 MyBatis</a>
<a href="transaction.html">🔄 事务</a>
<a href="users.html">👥 用户</a>
<a href="api.html">🔌 API</a>
</div>
<div class="card">
<h3>📚 核心概念</h3>
<p><strong>AOP (面向切面编程)</strong>:将横切关注点(日志、权限、事务等)从业务逻辑中分离出来,实现模块化。</p>
<div class="flow-diagram">
<div class="flow-step">目标方法</div>
<div class="flow-arrow"></div>
<div class="flow-step">@Before</div>
<div class="flow-arrow"></div>
<div class="flow-step">@Around (前)</div>
<div class="flow-arrow"></div>
<div class="flow-step">方法执行</div>
<div class="flow-arrow"></div>
<div class="flow-step">@Around (后)</div>
<div class="flow-arrow"></div>
<div class="flow-step">@AfterReturning</div>
<div class="flow-arrow"></div>
<div class="flow-step">@After</div>
</div>
</div>
<div class="card">
<h3>🔔 五种通知类型</h3>
<div class="advice-box advice-before">
<h4>@Before - 前置通知</h4>
<p>方法执行前触发,可用于参数校验、日志记录</p>
<div class="code-block">@Before("execution(* com.example.service..*.*(..))")<br>public void before(JoinPoint jp) {<br> log.info("即将执行: " + jp.getSignature().getName());<br>}</div>
</div>
<div class="advice-box advice-after">
<h4>@After - 后置通知</h4>
<p>方法执行后触发(无论是否异常),可用于资源释放</p>
<div class="code-block">@After("execution(* com.example.service..*.*(..))")<br>public void after(JoinPoint jp) {<br> log.info("执行完成: " + jp.getSignature().getName());<br>}</div>
</div>
<div class="advice-box advice-returning">
<h4>@AfterReturning - 返回通知</h4>
<p>方法成功返回后触发,可获取返回值</p>
<div class="code-block">@AfterReturning(pointcut="...", returning="result")<br>public void afterReturning(Object result) {<br> log.info("返回结果: " + result);<br>}</div>
</div>
<div class="advice-box advice-throwing">
<h4>@AfterThrowing - 异常通知</h4>
<p>方法抛出异常后触发,可用于异常处理</p>
<div class="code-block">@AfterThrowing(pointcut="...", throwing="ex")<br>public void afterThrowing(Exception ex) {<br> log.error("发生异常: " + ex.getMessage());<br>}</div>
</div>
<div class="advice-box advice-around">
<h4>@Around - 环绕通知</h4>
<p>完全控制方法执行,可决定是否执行目标方法</p>
<div class="code-block">@Around("execution(* com.example.controller..*.*(..))")<br>public Object around(ProceedingJoinPoint pjp) throws Throwable {<br> long start = System.currentTimeMillis();<br> Object result = pjp.proceed(); // 执行目标方法<br> long cost = System.currentTimeMillis() - start;<br> log.info("耗时: " + cost + "ms");<br> return result;<br>}</div>
</div>
</div>
<div class="card">
<h3>🧪 在线测试</h3>
<p>调用下面的 API然后查看控制台日志观察 AOP 执行顺序</p>
<button class="btn" onclick="testAop()">测试正常执行</button>
<button class="btn" onclick="testAopError()">测试异常通知</button>
<div id="testResult" class="result"></div>
</div>
<div class="card">
<h3>📝 切入点表达式</h3>
<table style="width:100%;border-collapse:collapse;">
<tr style="background:#f8f9fa;"><th style="padding:10px;text-align:left;">表达式</th><th style="padding:10px;text-align:left;">含义</th></tr>
<tr><td style="padding:10px;border-bottom:1px solid #eee;">execution(* *(..))</td><td style="padding:10px;border-bottom:1px solid #eee;">匹配所有方法</td></tr>
<tr><td style="padding:10px;border-bottom:1px solid #eee;">execution(* com.example.service.*.*(..))</td><td style="padding:10px;border-bottom:1px solid #eee;">service包下所有方法</td></tr>
<tr><td style="padding:10px;border-bottom:1px solid #eee;">execution(* com.example.service..*.*(..))</td><td style="padding:10px;border-bottom:1px solid #eee;">service包及子包所有方法</td></tr>
<tr><td style="padding:10px;border-bottom:1px solid #eee;">execution(public * *(..))</td><td style="padding:10px;border-bottom:1px solid #eee;">所有public方法</td></tr>
<tr><td style="padding:10px;border-bottom:1px solid #eee;">@annotation(org.springframework.transaction.annotation.Transactional)</td><td style="padding:10px;border-bottom:1px solid #eee;">带@Transactional的方法</td></tr>
</table>
</div>
</div>
<script>
async function testAop() {
const result = document.getElementById('testResult');
result.textContent = '测试中...';
try {
const res = await fetch('/api/learning/aop/test?message=HelloAOP');
const data = await res.json();
result.innerHTML = `<strong>✅ 测试成功</strong>\n\n${JSON.stringify(data, null, 2)}\n\n💡 提示: 查看服务器控制台,观察 AOP 通知执行顺序`;
} catch (e) {
result.textContent = '测试失败: ' + e.message;
}
}
async function testAopError() {
const result = document.getElementById('testResult');
result.textContent = '测试中...';
try {
const res = await fetch('/api/learning/aop/test-error?error=true');
const data = await res.json();
result.innerHTML = `<strong>✅ 正常返回</strong>\n\n${JSON.stringify(data, null, 2)}`;
} catch (e) {
result.innerHTML = `<strong>❌ 触发异常(预期行为)</strong>\n\n异常信息: ${e.message}\n\n💡 提示: 查看服务器控制台,观察 @AfterThrowing 通知`;
}
}
</script>
</body>
</html>

View File

@@ -0,0 +1,280 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>API 测试面板 - Spring Boot</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; }
.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 h1 { font-size: 2em; }
.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: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 h3 { color: #667eea; margin-bottom: 15px; border-bottom: 2px solid #eee; padding-bottom: 10px; }
.api-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); gap: 15px; }
.api-item { background: #f8f9fa; border-radius: 8px; padding: 15px; border-left: 4px solid; }
.api-item.get { border-color: #28a745; }
.api-item.post { border-color: #007bff; }
.api-item.put { border-color: #ffc107; }
.api-item.delete { border-color: #dc3545; }
.method { display: inline-block; padding: 3px 8px; border-radius: 4px; font-size: 0.75em; font-weight: bold; margin-right: 8px; }
.method.get { background: #28a745; color: white; }
.method.post { background: #007bff; color: white; }
.method.put { background: #ffc107; color: #333; }
.method.delete { background: #dc3545; color: white; }
.btn { padding: 8px 16px; border: none; border-radius: 5px; cursor: pointer; font-size: 0.9em; margin-top: 10px; }
.btn-primary { background: #667eea; color: white; }
.btn-primary:hover { background: #5a6fd6; }
.result { background: #1e1e1e; color: #d4d4d4; padding: 15px; border-radius: 5px; margin-top: 10px; font-family: monospace; font-size: 0.85em; overflow-x: auto; max-height: 200px; overflow-y: auto; display: none; }
.result.show { display: block; }
.url { font-family: monospace; color: #666; font-size: 0.9em; word-break: break-all; }
.tabs { display: flex; gap: 5px; margin-bottom: 20px; border-bottom: 2px solid #eee; }
.tab { padding: 10px 20px; cursor: pointer; border-bottom: 2px solid transparent; }
.tab.active { border-bottom-color: #667eea; color: #667eea; font-weight: 600; }
.tab-content { display: none; }
.tab-content.active { display: block; }
.json-input { width: 100%; min-height: 100px; padding: 10px; border: 1px solid #ddd; border-radius: 5px; font-family: monospace; font-size: 0.9em; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🔌 API 测试面板</h1>
<p>在线测试所有 RESTful API</p>
</div>
<div class="nav">
<a href="index.html">🏠 首页</a>
<a href="ioc.html">📦 IoC</a>
<a href="aop.html">🔪 AOP</a>
<a href="mybatis.html">💾 MyBatis</a>
<a href="transaction.html">🔄 事务</a>
<a href="users.html">👥 用户</a>
<a href="api.html" class="active">🔌 API</a>
</div>
<div class="tabs">
<div class="tab active" onclick="switchTab('user')">👥 用户 API</div>
<div class="tab" onclick="switchTab('product')">📦 产品 API</div>
<div class="tab" onclick="switchTab('order')">🛒 订单 API</div>
<div class="tab" onclick="switchTab('learning')">📚 学习 API</div>
</div>
<!-- 用户 API -->
<div id="userTab" class="tab-content active">
<div class="card">
<h3>用户管理 API</h3>
<div class="api-grid">
<div class="api-item get">
<span class="method get">GET</span>
<strong>获取所有用户</strong>
<div class="url">/api/users</div>
<button class="btn btn-primary" onclick="testApi('GET', '/api/users', null, 'userResult1')">测试</button>
<div id="userResult1" class="result"></div>
</div>
<div class="api-item get">
<span class="method get">GET</span>
<strong>获取单个用户</strong>
<div class="url">/api/users/{id}</div>
<input type="number" id="userId" placeholder="用户ID" style="width:80px;padding:5px;margin-top:5px;">
<button class="btn btn-primary" onclick="testApi('GET', '/api/users/' + document.getElementById('userId').value, null, 'userResult2')">测试</button>
<div id="userResult2" class="result"></div>
</div>
<div class="api-item post">
<span class="method post">POST</span>
<strong>创建用户</strong>
<div class="url">/api/users</div>
<textarea class="json-input" id="createUserJson">{
"username": "testuser",
"email": "test@example.com",
"phone": "13800138000",
"bio": "测试用户"
}</textarea>
<button class="btn btn-primary" onclick="testApi('POST', '/api/users', document.getElementById('createUserJson').value, 'userResult3')">测试</button>
<div id="userResult3" class="result"></div>
</div>
<div class="api-item get">
<span class="method get">GET</span>
<strong>搜索用户</strong>
<div class="url">/api/users/search?username={name}</div>
<input type="text" id="searchName" placeholder="用户名关键词" style="padding:5px;margin-top:5px;">
<button class="btn btn-primary" onclick="testApi('GET', '/api/users/search?username=' + encodeURIComponent(document.getElementById('searchName').value), null, 'userResult4')">测试</button>
<div id="userResult4" class="result"></div>
</div>
</div>
</div>
</div>
<!-- 产品 API -->
<div id="productTab" class="tab-content">
<div class="card">
<h3>产品管理 API</h3>
<div class="api-grid">
<div class="api-item get">
<span class="method get">GET</span>
<strong>获取所有产品</strong>
<div class="url">/api/products</div>
<button class="btn btn-primary" onclick="testApi('GET', '/api/products', null, 'productResult1')">测试</button>
<div id="productResult1" class="result"></div>
</div>
<div class="api-item post">
<span class="method post">POST</span>
<strong>创建产品</strong>
<div class="url">/api/products</div>
<textarea class="json-input" id="createProductJson">{
"name": "iPhone 15",
"description": "最新款苹果手机",
"price": 5999.00,
"stockQuantity": 100,
"category": "手机"
}</textarea>
<button class="btn btn-primary" onclick="testApi('POST', '/api/products', document.getElementById('createProductJson').value, 'productResult2')">测试</button>
<div id="productResult2" class="result"></div>
</div>
</div>
</div>
</div>
<!-- 订单 API -->
<div id="orderTab" class="tab-content">
<div class="card">
<h3>订单管理 API (演示事务)</h3>
<div class="api-grid">
<div class="api-item get">
<span class="method get">GET</span>
<strong>获取所有订单</strong>
<div class="url">/api/orders</div>
<button class="btn btn-primary" onclick="testApi('GET', '/api/orders', null, 'orderResult1')">测试</button>
<div id="orderResult1" class="result"></div>
</div>
<div class="api-item post">
<span class="method post">POST</span>
<strong>创建订单</strong>
<div class="url">/api/orders</div>
<textarea class="json-input" id="createOrderJson">{
"userId": 1,
"productId": 1,
"quantity": 1,
"rollback": false
}</textarea>
<button class="btn btn-primary" onclick="testApi('POST', '/api/orders', document.getElementById('createOrderJson').value, 'orderResult2')">测试</button>
<div id="orderResult2" class="result"></div>
</div>
<div class="api-item post">
<span class="method post">POST</span>
<strong>创建订单(触发回滚)</strong>
<div class="url">/api/orders (rollback=true)</div>
<textarea class="json-input" id="rollbackOrderJson">{
"userId": 1,
"productId": 1,
"quantity": 1,
"rollback": true
}</textarea>
<button class="btn btn-primary" onclick="testApi('POST', '/api/orders', document.getElementById('rollbackOrderJson').value, 'orderResult3')">测试</button>
<div id="orderResult3" class="result"></div>
</div>
</div>
</div>
</div>
<!-- 学习 API -->
<div id="learningTab" class="tab-content">
<div class="card">
<h3>学习 API</h3>
<div class="api-grid">
<div class="api-item get">
<span class="method get">GET</span>
<strong>IoC - 查看所有 Bean</strong>
<div class="url">/api/learning/ioc/beans</div>
<button class="btn btn-primary" onclick="testApi('GET', '/api/learning/ioc/beans', null, 'learnResult1')">测试</button>
<div id="learnResult1" class="result"></div>
</div>
<div class="api-item get">
<span class="method get">GET</span>
<strong>AOP - 概念</strong>
<div class="url">/api/learning/aop/concepts</div>
<button class="btn btn-primary" onclick="testApi('GET', '/api/learning/aop/concepts', null, 'learnResult2')">测试</button>
<div id="learnResult2" class="result"></div>
</div>
<div class="api-item get">
<span class="method get">GET</span>
<strong>MyBatis - 配置</strong>
<div class="url">/api/learning/mybatis/config</div>
<button class="btn btn-primary" onclick="testApi('GET', '/api/learning/mybatis/config', null, 'learnResult3')">测试</button>
<div id="learnResult3" class="result"></div>
</div>
<div class="api-item get">
<span class="method get">GET</span>
<strong>事务 - 传播行为</strong>
<div class="url">/api/learning/transaction/propagation</div>
<button class="btn btn-primary" onclick="testApi('GET', '/api/learning/transaction/propagation', null, 'learnResult4')">测试</button>
<div id="learnResult4" class="result"></div>
</div>
</div>
</div>
</div>
</div>
<script>
function switchTab(tab) {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
event.target.classList.add('active');
document.getElementById(tab + 'Tab').classList.add('active');
}
async function testApi(method, url, body, resultId) {
const resultDiv = document.getElementById(resultId);
resultDiv.classList.add('show');
resultDiv.textContent = '请求中...';
try {
const options = {
method: method,
headers: {}
};
if (body) {
options.headers['Content-Type'] = 'application/json';
options.body = body;
}
const startTime = Date.now();
const res = await fetch(url, options);
const duration = Date.now() - startTime;
const data = await res.json();
resultDiv.innerHTML = `<strong>${res.status} ${res.statusText}</strong> (${duration}ms)\n\n${JSON.stringify(data, null, 2)}`;
} catch (e) {
resultDiv.innerHTML = `<strong style="color:#ff6b6b;">Error</strong>\n\n${e.message}`;
}
}
</script>
</body>
</html>

View File

@@ -0,0 +1,203 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Spring Boot 学习中心</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; }
.container { max-width: 1400px; margin: 0 auto; padding: 20px; }
.header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 40px 20px; text-align: center; margin-bottom: 30px; border-radius: 10px; }
.header h1 { font-size: 2.5em; margin-bottom: 10px; }
.header p { opacity: 0.9; font-size: 1.1em; }
.nav { display: flex; gap: 10px; margin-bottom: 30px; flex-wrap: wrap; justify-content: center; }
.nav a { padding: 12px 24px; background: white; border-radius: 25px; text-decoration: none; color: #333; font-weight: 500; transition: all 0.3s; border: 2px solid #e0e0e0; }
.nav a:hover, .nav a.active { background: #667eea; color: white; border-color: #667eea; }
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; }
.card { background: white; border-radius: 15px; padding: 25px; box-shadow: 0 5px 20px rgba(0,0,0,0.08); transition: transform 0.3s; }
.card:hover { transform: translateY(-5px); }
.card h3 { color: #333; margin-bottom: 15px; font-size: 1.3em; }
.card p { color: #666; line-height: 1.6; margin-bottom: 15px; }
.card .btn { display: inline-block; padding: 10px 20px; background: #667eea; color: white; text-decoration: none; border-radius: 20px; font-size: 0.9em; }
.card .btn:hover { background: #5a6fd6; }
.feature-list { list-style: none; }
.feature-list li { padding: 8px 0; border-bottom: 1px solid #eee; }
.feature-list li:last-child { border-bottom: none; }
.feature-list .icon { margin-right: 8px; }
.api-test { background: #1e1e1e; border-radius: 10px; padding: 20px; margin-top: 20px; }
.api-test h4 { color: #4ec9b0; margin-bottom: 15px; }
.api-test pre { color: #d4d4d4; overflow-x: auto; font-size: 0.85em; }
.api-test .method { color: #569cd6; }
.api-test .url { color: #4ec9b0; }
.status { display: flex; gap: 20px; flex-wrap: wrap; margin-bottom: 30px; }
.status-item { background: white; padding: 20px 30px; border-radius: 10px; text-align: center; }
.status-item .value { font-size: 2em; font-weight: bold; color: #667eea; }
.status-item .label { color: #666; margin-top: 5px; }
footer { text-align: center; padding: 30px; color: #666; margin-top: 40px; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🍃 Spring Boot 学习中心</h1>
<p>交互式学习 Spring 核心功能 | IoC · AOP · MyBatis · 事务</p>
</div>
<div class="status">
<div class="status-item">
<div class="value" id="beanCount">-</div>
<div class="label">已加载 Bean</div>
</div>
<div class="status-item">
<div class="value" id="userCount">-</div>
<div class="label">用户数量</div>
</div>
<div class="status-item">
<div class="value" id="productCount">-</div>
<div class="label">产品数量</div>
</div>
<div class="status-item">
<div class="value" id="orderCount">-</div>
<div class="label">订单数量</div>
</div>
</div>
<div class="nav">
<a href="index.html" class="active">🏠 首页</a>
<a href="ioc.html">📦 IoC 容器</a>
<a href="aop.html">🔪 AOP 切面</a>
<a href="mybatis.html">💾 MyBatis</a>
<a href="transaction.html">🔄 事务管理</a>
<a href="users.html">👥 用户管理</a>
<a href="api.html">🔌 API 测试</a>
</div>
<div class="grid">
<div class="card">
<h3>📦 IoC 容器</h3>
<p>理解 Spring 的核心:控制反转和依赖注入。学习 Bean 的生命周期、作用域和各种注入方式。</p>
<ul class="feature-list">
<li><span class="icon"></span>Bean 生命周期演示</li>
<li><span class="icon"></span>依赖注入方式对比</li>
<li><span class="icon"></span>Bean 作用域详解</li>
<li><span class="icon"></span>性能统计面板</li>
</ul>
<a href="ioc.html" class="btn">开始学习 →</a>
</div>
<div class="card">
<h3>🔪 AOP 切面编程</h3>
<p>掌握面向切面编程,实现日志、性能监控、事务等横切关注点的模块化管理。</p>
<ul class="feature-list">
<li><span class="icon"></span>5 种通知类型演示</li>
<li><span class="icon"></span>切入点表达式语法</li>
<li><span class="icon"></span>实时日志展示</li>
<li><span class="icon"></span>性能监控切面</li>
</ul>
<a href="aop.html" class="btn">开始学习 →</a>
</div>
<div class="card">
<h3>💾 MyBatis 集成</h3>
<p>学习 MyBatis 与 Spring Boot 的整合,对比 JPA掌握动态 SQL 和缓存机制。</p>
<ul class="feature-list">
<li><span class="icon"></span>MyBatis vs JPA 对比</li>
<li><span class="icon"></span>动态 SQL 语法</li>
<li><span class="icon"></span>一级/二级缓存演示</li>
<li><span class="icon"></span>批量操作示例</li>
</ul>
<a href="mybatis.html" class="btn">开始学习 →</a>
</div>
<div class="card">
<h3>🔄 事务管理</h3>
<p>深入理解 Spring 声明式事务,掌握传播行为和隔离级别的实际应用。</p>
<ul class="feature-list">
<li><span class="icon"></span>事务传播行为</li>
<li><span class="icon"></span>事务隔离级别</li>
<li><span class="icon"></span>回滚机制演示</li>
<li><span class="icon"></span>订单创建场景</li>
</ul>
<a href="transaction.html" class="btn">开始学习 →</a>
</div>
<div class="card">
<h3>👥 用户管理 CRUD</h3>
<p>完整的 RESTful API 示例,演示增删改查操作和参数验证。</p>
<ul class="feature-list">
<li><span class="icon"></span>RESTful 设计规范</li>
<li><span class="icon"></span>参数验证</li>
<li><span class="icon"></span>异常处理</li>
<li><span class="icon"></span>交互式测试</li>
</ul>
<a href="users.html" class="btn">开始学习 →</a>
</div>
<div class="card">
<h3>🔌 API 测试面板</h3>
<p>在线测试所有 API 接口,查看请求响应,理解 RESTful API 工作原理。</p>
<ul class="feature-list">
<li><span class="icon"></span>用户 API</li>
<li><span class="icon"></span>产品 API</li>
<li><span class="icon"></span>订单 API</li>
<li><span class="icon"></span>学习 API</li>
</ul>
<a href="api.html" class="btn">开始测试 →</a>
</div>
</div>
<div class="api-test">
<h4>🚀 快速开始 - API 示例</h4>
<pre>
<span class="method">GET</span> <span class="url">/api/users</span> # 获取所有用户
<span class="method">GET</span> <span class="url">/api/users/{id}</span> # 获取单个用户
<span class="method">POST</span> <span class="url">/api/users</span> # 创建用户
<span class="method">PUT</span> <span class="url">/api/users/{id}</span> # 更新用户
<span class="method">DEL</span> <span class="url">/api/users/{id}</span> # 删除用户
<span class="method">GET</span> <span class="url">/api/learning/ioc/beans</span> # 查看所有 Bean
<span class="method">GET</span> <span class="url">/api/learning/aop/concepts</span> # AOP 概念
<span class="method">GET</span> <span class="url">/api/learning/mybatis/cache</span> # 缓存机制
<span class="method">GET</span> <span class="url">/api/learning/transaction/propagation</span> # 传播行为
</pre>
</div>
<footer>
<p>🍃 Spring Boot 学习脚手架 | <a href="/h2-console" target="_blank">H2 控制台</a> (JDBC: jdbc:h2:file:~/h2/springboot_scaffold, 用户: sa)</p>
</footer>
</div>
<script>
// 加载状态数据
async function loadStatus() {
try {
const [beans, users, products, orders] = await Promise.all([
fetch('/api/learning/ioc/beans').then(r => r.json()),
fetch('/api/users/count').then(r => r.json()),
fetch('/api/products').then(r => r.json()),
fetch('/api/orders').then(r => r.json())
]);
document.getElementById('beanCount').textContent = beans.total || '-';
document.getElementById('userCount').textContent = users.count || 0;
document.getElementById('productCount').textContent = products.length || 0;
document.getElementById('orderCount').textContent = orders.length || 0;
} catch (e) {
console.error('加载状态失败:', e);
}
}
loadStatus();
setInterval(loadStatus, 30000);
</script>
</body>
</html>

View File

@@ -0,0 +1,175 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>IoC 容器学习 - Spring Boot</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; }
.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 h1 { font-size: 2em; }
.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: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 h3 { color: #667eea; margin-bottom: 15px; border-bottom: 2px solid #eee; padding-bottom: 10px; }
.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; }
.btn { padding: 10px 20px; background: #667eea; color: white; border: none; border-radius: 5px; cursor: pointer; margin: 5px; }
.btn:hover { background: #5a6fd6; }
.btn-secondary { background: #6c757d; }
.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; }
table { width: 100%; border-collapse: collapse; margin: 10px 0; }
th, td { padding: 10px; text-align: left; border-bottom: 1px solid #eee; }
th { background: #f8f9fa; font-weight: 600; }
tr:hover { background: #f8f9fa; }
.badge { padding: 4px 8px; border-radius: 4px; font-size: 0.8em; }
.badge-primary { background: #667eea; color: white; }
.badge-success { background: #28a745; color: white; }
.badge-warning { background: #ffc107; color: #333; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>📦 IoC 容器学习</h1>
<p>控制反转 (Inversion of Control) 与依赖注入 (Dependency Injection)</p>
</div>
<div class="nav">
<a href="index.html">🏠 首页</a>
<a href="ioc.html" class="active">📦 IoC</a>
<a href="aop.html">🔪 AOP</a>
<a href="mybatis.html">💾 MyBatis</a>
<a href="transaction.html">🔄 事务</a>
<a href="users.html">👥 用户</a>
<a href="api.html">🔌 API</a>
</div>
<div class="card">
<h3>📚 核心概念</h3>
<div class="concept-box">
<h4>什么是 IoC</h4>
<p><strong>控制反转 (Inversion of Control)</strong>:将对象的创建和管理交给 Spring 容器,而不是由开发者手动创建。</p>
<p><strong>依赖注入 (Dependency Injection)</strong>IoC 的一种实现方式通过构造器、Setter 或字段将依赖注入到对象中。</p>
</div>
<div class="concept-box">
<h4>为什么用 IoC</h4>
<ul>
<li><strong>解耦</strong>:对象之间不直接依赖,通过接口交互</li>
<li><strong>可测试</strong>:方便使用 Mock 对象进行单元测试</li>
<li><strong>可维护</strong>:集中管理对象生命周期</li>
<li><strong>AOP 支持</strong>:便于实现切面编程</li>
</ul>
</div>
</div>
<div class="card">
<h3>🔍 查看所有 Bean</h3>
<p>Spring 容器中管理的所有 Bean 对象</p>
<button class="btn" onclick="loadBeans()">刷新 Bean 列表</button>
<button class="btn btn-secondary" onclick="document.getElementById('beansResult').innerHTML=''">清空</button>
<div id="beansResult" class="result"></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>
<script>
async function loadBeans() {
const result = document.getElementById('beansResult');
result.textContent = '加载中...';
try {
const res = await fetch('/api/learning/ioc/beans');
const data = await res.json();
result.innerHTML = `<strong>总 Bean 数: ${data.total}</strong>\n\n用户相关 Bean:\n${data.userBeans.map(b => ' - ' + b).join('\n')}`;
} catch (e) {
result.textContent = '加载失败: ' + e.message;
}
}
async function testScopes() {
const result = document.getElementById('scopesResult');
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>
</body>
</html>

View File

@@ -0,0 +1,176 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MyBatis 学习 - Spring Boot</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; }
.container { max-width: 1400px; margin: 0 auto; padding: 20px; }
.header { background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); color: white; padding: 30px 20px; text-align: center; margin-bottom: 20px; border-radius: 10px; }
.header h1 { font-size: 2em; }
.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:hover, .nav a.active { background: #4facfe; 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 h3 { color: #4facfe; margin-bottom: 15px; border-bottom: 2px solid #eee; padding-bottom: 10px; }
.comparison { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
.compare-box { padding: 20px; border-radius: 10px; }
.mybatis-box { background: #e3f2fd; border: 2px solid #4facfe; }
.jpa-box { background: #f3e5f5; border: 2px solid #9c27b0; }
.btn { padding: 10px 20px; background: #4facfe; color: white; border: none; border-radius: 5px; cursor: pointer; margin: 5px; }
.btn:hover { background: #3d9be8; }
.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; }
.code-block { background: #f4f4f4; padding: 15px; border-radius: 5px; font-family: monospace; font-size: 0.9em; overflow-x: auto; margin: 10px 0; }
.cache-box { background: #fff3e0; border-left: 4px solid #ff9800; padding: 15px; margin: 10px 0; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>💾 MyBatis 学习</h1>
<p>半自动 ORM 框架 - SQL 与 Java 对象的映射</p>
</div>
<div class="nav">
<a href="index.html">🏠 首页</a>
<a href="ioc.html">📦 IoC</a>
<a href="aop.html">🔪 AOP</a>
<a href="mybatis.html" class="active">💾 MyBatis</a>
<a href="transaction.html">🔄 事务</a>
<a href="users.html">👥 用户</a>
<a href="api.html">🔌 API</a>
</div>
<div class="card">
<h3>📊 MyBatis vs JPA 对比</h3>
<div class="comparison">
<div class="compare-box mybatis-box">
<h4>MyBatis</h4>
<p><strong>优点:</strong></p>
<ul>
<li>✅ SQL 灵活可控</li>
<li>✅ 性能优化方便</li>
<li>✅ 复杂查询友好</li>
<li>✅ 易于 DBA 协作</li>
</ul>
<p><strong>缺点:</strong></p>
<ul>
<li>❌ SQL 与代码耦合</li>
<li>❌ 数据库迁移成本高</li>
<li>❌ 需要手写 SQL</li>
</ul>
<p><strong>适用:</strong>复杂查询、性能要求高</p>
</div>
<div class="compare-box jpa-box">
<h4>JPA/Hibernate</h4>
<p><strong>优点:</strong></p>
<ul>
<li>✅ 面向对象</li>
<li>✅ 数据库无关</li>
<li>✅ 开发效率高</li>
<li>✅ 自动维护</li>
</ul>
<p><strong>缺点:</strong></p>
<ul>
<li>❌ 复杂查询困难</li>
<li>❌ 性能调优复杂</li>
<li>❌ 学习曲线陡</li>
</ul>
<p><strong>适用:</strong>标准 CRUD、快速开发</p>
</div>
</div>
</div>
<div class="card">
<h3>🚀 快速体验</h3>
<button class="btn" onclick="loadConfig()">查看 MyBatis 配置</button>
<button class="btn" onclick="loadConcepts()">核心概念</button>
<button class="btn" onclick="loadVsJpa()">详细对比</button>
<div id="result" class="result"></div>
</div>
<div class="card">
<h3>💡 缓存机制</h3>
<div class="cache-box">
<h4>一级缓存 (SqlSession 级别)</h4>
<p>默认开启,同一 SqlSession 内相同查询只执行一次 SQL</p>
<div class="code-block">// 第一次查询 - 执行 SQL<br>User u1 = mapper.findById(1L);<br>// 第二次查询 - 命中缓存,不执行 SQL<br>User u2 = mapper.findById(1L);</div>
</div>
<div class="cache-box">
<h4>二级缓存 (Mapper 级别)</h4>
<p>需配置 @CacheNamespace多个 SqlSession 共享</p>
<div class="code-block">@Mapper<br>@CacheNamespace<br>public interface UserMapper { ... }</div>
</div>
<p>💡 <strong>验证方式:</strong>查看控制台 SQL 日志,缓存命中时不打印 SQL</p>
</div>
<div class="card">
<h3>📝 常用注解</h3>
<table style="width:100%;border-collapse:collapse;">
<tr style="background:#f8f9fa;"><th style="padding:10px;text-align:left;">注解</th><th style="padding:10px;text-align:left;">用途</th></tr>
<tr><td style="padding:10px;border-bottom:1px solid #eee;">@Mapper</td><td style="padding:10px;border-bottom:1px solid #eee;">标记为 MyBatis Mapper 接口</td></tr>
<tr><td style="padding:10px;border-bottom:1px solid #eee;">@Select</td><td style="padding:10px;border-bottom:1px solid #eee;">查询 SQL</td></tr>
<tr><td style="padding:10px;border-bottom:1px solid #eee;">@Insert</td><td style="padding:10px;border-bottom:1px solid #eee;">插入 SQL</td></tr>
<tr><td style="padding:10px;border-bottom:1px solid #eee;">@Update</td><td style="padding:10px;border-bottom:1px solid #eee;">更新 SQL</td></tr>
<tr><td style="padding:10px;border-bottom:1px solid #eee;">@Delete</td><td style="padding:10px;border-bottom:1px solid #eee;">删除 SQL</td></tr>
<tr><td style="padding:10px;border-bottom:1px solid #eee;">@Param</td><td style="padding:10px;border-bottom:1px solid #eee;">参数命名</td></tr>
<tr><td style="padding:10px;border-bottom:1px solid #eee;">@Options</td><td style="padding:10px;border-bottom:1px solid #eee;">额外选项如返回自增ID</td></tr>
<tr><td style="padding:10px;border-bottom:1px solid #eee;">@CacheNamespace</td><td style="padding:10px;border-bottom:1px solid #eee;">启用二级缓存</td></tr>
</table>
</div>
<div class="card">
<h3>🔧 动态 SQL 示例</h3>
<div class="code-block">&lt;select id="findUsers"&gt;<br> SELECT * FROM users<br> &lt;where&gt;<br> &lt;if test="name != null"&gt;<br> AND name LIKE CONCAT('%', #{name}, '%')<br> &lt;/if&gt;<br> &lt;if test="email != null"&gt;<br> AND email = #{email}<br> &lt;/if&gt;<br> &lt;/where&gt;<br>&lt;/select&gt;</div>
</div>
</div>
<script>
async function loadConfig() {
const result = document.getElementById('result');
result.textContent = '加载中...';
try {
const res = await fetch('/api/learning/mybatis/config');
const data = await res.json();
result.innerHTML = `<strong>MyBatis 配置</strong>\n\n${JSON.stringify(data, null, 2)}`;
} catch (e) {
result.textContent = '加载失败: ' + e.message;
}
}
async function loadConcepts() {
const result = document.getElementById('result');
result.textContent = '加载中...';
try {
const res = await fetch('/api/learning/mybatis/concepts');
const data = await res.json();
result.innerHTML = `<strong>核心概念</strong>\n\n${JSON.stringify(data, null, 2)}`;
} catch (e) {
result.textContent = '加载失败: ' + e.message;
}
}
async function loadVsJpa() {
const result = document.getElementById('result');
result.textContent = '加载中...';
try {
const res = await fetch('/api/learning/mybatis/vs-jpa');
const data = await res.json();
result.innerHTML = `<strong>MyBatis vs JPA</strong>\n\n${JSON.stringify(data, null, 2)}`;
} catch (e) {
result.textContent = '加载失败: ' + e.message;
}
}
</script>
</body>
</html>

View File

@@ -0,0 +1,191 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>事务管理学习 - Spring Boot</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; }
.container { max-width: 1400px; margin: 0 auto; padding: 20px; }
.header { background: linear-gradient(135deg, #fa709a 0%, #fee140 100%); color: white; padding: 30px 20px; text-align: center; margin-bottom: 20px; border-radius: 10px; }
.header h1 { font-size: 2em; }
.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:hover, .nav a.active { background: #fa709a; 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 h3 { color: #fa709a; margin-bottom: 15px; border-bottom: 2px solid #eee; padding-bottom: 10px; }
.acid-box { display: grid; grid-template-columns: repeat(4, 1fr); gap: 15px; margin: 20px 0; }
.acid-item { background: #f8f9fa; padding: 20px; border-radius: 10px; text-align: center; border-top: 4px solid; }
.acid-a { border-color: #e74c3c; }
.acid-c { border-color: #3498db; }
.acid-i { border-color: #2ecc71; }
.acid-d { border-color: #9b59b6; }
.acid-item h4 { font-size: 2em; margin-bottom: 10px; }
.btn { padding: 10px 20px; background: #fa709a; color: white; border: none; border-radius: 5px; cursor: pointer; margin: 5px; }
.btn:hover { background: #e85a8a; }
.btn-danger { background: #e74c3c; }
.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; }
.propagation-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 15px; }
.prop-box { background: #f8f9fa; padding: 15px; border-radius: 8px; border-left: 4px solid #fa709a; }
.prop-box h4 { color: #fa709a; margin-bottom: 8px; }
table { width: 100%; border-collapse: collapse; margin: 15px 0; }
th, td { padding: 12px; text-align: left; border-bottom: 1px solid #eee; }
th { background: #f8f9fa; font-weight: 600; }
tr:hover { background: #f8f9fa; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🔄 事务管理</h1>
<p>声明式事务 - @Transactional</p>
</div>
<div class="nav">
<a href="index.html">🏠 首页</a>
<a href="ioc.html">📦 IoC</a>
<a href="aop.html">🔪 AOP</a>
<a href="mybatis.html">💾 MyBatis</a>
<a href="transaction.html" class="active">🔄 事务</a>
<a href="users.html">👥 用户</a>
<a href="api.html">🔌 API</a>
</div>
<div class="card">
<h3>📚 ACID 特性</h3>
<div class="acid-box">
<div class="acid-item acid-a">
<h4>A</h4>
<p><strong>Atomicity</strong></p>
<p>原子性</p>
<p style="font-size:0.9em;color:#666;">事务是不可分割的工作单位</p>
</div>
<div class="acid-item acid-c">
<h4>C</h4>
<p><strong>Consistency</strong></p>
<p>一致性</p>
<p style="font-size:0.9em;color:#666;">数据库状态保持一致</p>
</div>
<div class="acid-item acid-i">
<h4>I</h4>
<p><strong>Isolation</strong></p>
<p>隔离性</p>
<p style="font-size:0.9em;color:#666;">事务之间相互隔离</p>
</div>
<div class="acid-item acid-d">
<h4>D</h4>
<p><strong>Durability</strong></p>
<p>持久性</p>
<p style="font-size:0.9em;color:#666;">提交后永久保存</p>
</div>
</div>
</div>
<div class="card">
<h3>🚀 传播行为 (Propagation)</h3>
<div class="propagation-grid">
<div class="prop-box">
<h4>REQUIRED (默认)</h4>
<p>有事务则加入,无则新建</p>
<p style="font-size:0.85em;color:#666;">最常用,适合大多数业务方法</p>
</div>
<div class="prop-box">
<h4>REQUIRES_NEW</h4>
<p>总是新建事务,挂起当前事务</p>
<p style="font-size:0.85em;color:#666;">适合日志记录、独立子任务</p>
</div>
<div class="prop-box">
<h4>SUPPORTS</h4>
<p>有事务则加入,无则以非事务运行</p>
<p style="font-size:0.85em;color:#666;">适合查询方法</p>
</div>
<div class="prop-box">
<h4>NOT_SUPPORTED</h4>
<p>以非事务运行,挂起当前事务</p>
<p style="font-size:0.85em;color:#666;">不需要事务的操作</p>
</div>
<div class="prop-box">
<h4>MANDATORY</h4>
<p>必须在事务中运行,否则抛异常</p>
<p style="font-size:0.85em;color:#666;">强制要求事务</p>
</div>
<div class="prop-box">
<h4>NEVER</h4>
<p>不能在事务中运行,否则抛异常</p>
<p style="font-size:0.85em;color:#666;">确保无事务</p>
</div>
</div>
<button class="btn" onclick="loadPropagation()">查看详细说明</button>
<div id="propResult" class="result"></div>
</div>
<div class="card">
<h3>🔒 隔离级别 (Isolation)</h3>
<table>
<tr><th>级别</th><th>脏读</th><th>不可重复读</th><th>幻读</th><th>说明</th></tr>
<tr><td>READ_UNCOMMITTED</td><td></td><td></td><td></td><td>读未提交,性能最高</td></tr>
<tr><td>READ_COMMITTED</td><td></td><td></td><td></td><td>读已提交Oracle默认</td></tr>
<tr><td>REPEATABLE_READ</td><td></td><td></td><td></td><td>可重复读MySQL默认</td></tr>
<tr><td>SERIALIZABLE</td><td></td><td></td><td></td><td>串行化,性能最低</td></tr>
</table>
<p style="margin-top:10px;color:#666;">✅ = 防止该问题 | ❌ = 可能出现该问题</p>
<button class="btn" onclick="loadIsolation()">查看详细说明</button>
<div id="isoResult" class="result"></div>
</div>
<div class="card">
<h3>🧪 事务回滚演示</h3>
<p>创建订单时触发异常,观察事务回滚效果</p>
<button class="btn" onclick="loadRollback()">查看回滚规则</button>
<div id="rollbackResult" class="result"></div>
</div>
</div>
<script>
async function loadPropagation() {
const result = document.getElementById('propResult');
result.textContent = '加载中...';
try {
const res = await fetch('/api/learning/transaction/propagation');
const data = await res.json();
result.innerHTML = `<strong>传播行为详解</strong>\n\n${JSON.stringify(data, null, 2)}`;
} catch (e) {
result.textContent = '加载失败: ' + e.message;
}
}
async function loadIsolation() {
const result = document.getElementById('isoResult');
result.textContent = '加载中...';
try {
const res = await fetch('/api/learning/transaction/isolation');
const data = await res.json();
result.innerHTML = `<strong>隔离级别详解</strong>\n\n${JSON.stringify(data, null, 2)}`;
} catch (e) {
result.textContent = '加载失败: ' + e.message;
}
}
async function loadRollback() {
const result = document.getElementById('rollbackResult');
result.textContent = '加载中...';
try {
const res = await fetch('/api/learning/transaction/rollback');
const data = await res.json();
result.innerHTML = `<strong>回滚规则</strong>\n\n${JSON.stringify(data, null, 2)}`;
} catch (e) {
result.textContent = '加载失败: ' + e.message;
}
}
</script>
</body>
</html>

View File

@@ -0,0 +1,278 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>用户管理 - Spring Boot CRUD</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; }
.container { max-width: 1400px; margin: 0 auto; padding: 20px; }
.header { background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%); color: white; padding: 30px 20px; text-align: center; margin-bottom: 20px; border-radius: 10px; }
.header h1 { font-size: 2em; }
.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:hover, .nav a.active { background: #11998e; 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 h3 { color: #11998e; margin-bottom: 15px; border-bottom: 2px solid #eee; padding-bottom: 10px; }
.form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 15px; margin-bottom: 20px; }
.form-group { display: flex; flex-direction: column; }
.form-group label { margin-bottom: 5px; color: #666; font-size: 0.9em; }
.form-group input, .form-group textarea { padding: 10px; border: 1px solid #ddd; border-radius: 5px; font-size: 1em; }
.form-group input:focus, .form-group textarea:focus { outline: none; border-color: #11998e; }
.btn { padding: 10px 20px; border: none; border-radius: 5px; cursor: pointer; font-size: 1em; margin: 5px; }
.btn-primary { background: #11998e; color: white; }
.btn-primary:hover { background: #0d7a6e; }
.btn-secondary { background: #6c757d; color: white; }
.btn-danger { background: #dc3545; color: white; }
.btn-warning { background: #ffc107; color: #333; }
table { width: 100%; border-collapse: collapse; margin-top: 20px; }
th, td { padding: 12px; text-align: left; border-bottom: 1px solid #eee; }
th { background: #f8f9fa; font-weight: 600; }
tr:hover { background: #f8f9fa; }
.actions { display: flex; gap: 5px; }
.actions button { padding: 5px 10px; font-size: 0.85em; }
.badge { padding: 4px 8px; border-radius: 4px; font-size: 0.8em; }
.badge-active { background: #28a745; color: white; }
.badge-inactive { background: #6c757d; color: white; }
.search-box { display: flex; gap: 10px; margin-bottom: 20px; }
.search-box input { flex: 1; padding: 10px; border: 1px solid #ddd; border-radius: 5px; }
.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: 300px; overflow-y: auto; }
.result.success { background: #d4edda; color: #155724; }
.result.error { background: #f8d7da; color: #721c24; }
.modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 1000; }
.modal-content { background: white; margin: 50px auto; padding: 30px; width: 90%; max-width: 600px; border-radius: 10px; }
.modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
.modal-header h3 { margin: 0; border: none; }
.close { font-size: 1.5em; cursor: pointer; color: #999; }
.close:hover { color: #333; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>👥 用户管理</h1>
<p>RESTful CRUD 操作演示</p>
</div>
<div class="nav">
<a href="index.html">🏠 首页</a>
<a href="ioc.html">📦 IoC</a>
<a href="aop.html">🔪 AOP</a>
<a href="mybatis.html">💾 MyBatis</a>
<a href="transaction.html">🔄 事务</a>
<a href="users.html" class="active">👥 用户</a>
<a href="api.html">🔌 API</a>
</div>
<div class="card">
<h3> 创建/编辑用户</h3>
<form id="userForm">
<input type="hidden" id="userId">
<div class="form-grid">
<div class="form-group">
<label>用户名 *</label>
<input type="text" id="username" required placeholder="输入用户名">
</div>
<div class="form-group">
<label>邮箱 *</label>
<input type="email" id="email" required placeholder="输入邮箱">
</div>
<div class="form-group">
<label>手机号</label>
<input type="text" id="phone" placeholder="输入手机号">
</div>
<div class="form-group">
<label>状态</label>
<select id="active" style="padding:10px;border:1px solid #ddd;border-radius:5px;">
<option value="true">启用</option>
<option value="false">禁用</option>
</select>
</div>
</div>
<div class="form-group" style="margin-bottom:15px;">
<label>简介</label>
<textarea id="bio" rows="3" placeholder="输入个人简介"></textarea>
</div>
<button type="submit" class="btn btn-primary">💾 保存</button>
<button type="button" class="btn btn-secondary" onclick="resetForm()">🔄 重置</button>
</form>
<div id="formResult"></div>
</div>
<div class="card">
<h3>📋 用户列表</h3>
<div class="search-box">
<input type="text" id="searchInput" placeholder="搜索用户名...">
<button class="btn btn-primary" onclick="searchUsers()">🔍 搜索</button>
<button class="btn btn-secondary" onclick="loadUsers()">🔄 刷新</button>
</div>
<table>
<thead>
<tr>
<th>ID</th>
<th>用户名</th>
<th>邮箱</th>
<th>手机号</th>
<th>状态</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody id="userTable">
<tr><td colspan="7" style="text-align:center;color:#999;">加载中...</td></tr>
</tbody>
</table>
</div>
</div>
<script>
let users = [];
async function loadUsers() {
const tbody = document.getElementById('userTable');
tbody.innerHTML = '<tr><td colspan="7" style="text-align:center;color:#999;">加载中...</td></tr>';
try {
const res = await fetch('/api/users');
users = await res.json();
renderUsers(users);
} catch (e) {
tbody.innerHTML = `<tr><td colspan="7" style="text-align:center;color:#dc3545;">加载失败: ${e.message}</td></tr>`;
}
}
function renderUsers(data) {
const tbody = document.getElementById('userTable');
if (data.length === 0) {
tbody.innerHTML = '<tr><td colspan="7" style="text-align:center;color:#999;">暂无数据</td></tr>';
return;
}
tbody.innerHTML = data.map(u => `
<tr>
<td>${u.id}</td>
<td>${u.username}</td>
<td>${u.email}</td>
<td>${u.phone || '-'}</td>
<td><span class="badge ${u.active ? 'badge-active' : 'badge-inactive'}">${u.active ? '启用' : '禁用'}</span></td>
<td>${u.createdAt ? new Date(u.createdAt).toLocaleString() : '-'}</td>
<td class="actions">
<button class="btn btn-warning" onclick="editUser(${u.id})">✏️ 编辑</button>
<button class="btn btn-danger" onclick="deleteUser(${u.id})">🗑️ 删除</button>
</td>
</tr>
`).join('');
}
async function searchUsers() {
const keyword = document.getElementById('searchInput').value.trim();
if (!keyword) {
loadUsers();
return;
}
try {
const res = await fetch(`/api/users/search?username=${encodeURIComponent(keyword)}`);
const data = await res.json();
renderUsers(data);
} catch (e) {
alert('搜索失败: ' + e.message);
}
}
document.getElementById('userForm').addEventListener('submit', async (e) => {
e.preventDefault();
const userId = document.getElementById('userId').value;
const user = {
username: document.getElementById('username').value,
email: document.getElementById('email').value,
phone: document.getElementById('phone').value,
bio: document.getElementById('bio').value,
active: document.getElementById('active').value === 'true'
};
const resultDiv = document.getElementById('formResult');
resultDiv.className = 'result';
resultDiv.textContent = '保存中...';
try {
const url = userId ? `/api/users/${userId}` : '/api/users';
const method = userId ? 'PUT' : 'POST';
const res = await fetch(url, {
method: method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(user)
});
const data = await res.json();
if (res.ok) {
resultDiv.className = 'result success';
resultDiv.innerHTML = `<strong>✅ 保存成功!</strong><br>ID: ${data.id}, 用户名: ${data.username}`;
resetForm();
loadUsers();
} else {
resultDiv.className = 'result error';
resultDiv.innerHTML = `<strong>❌ 保存失败</strong><br>${JSON.stringify(data)}`;
}
} catch (e) {
resultDiv.className = 'result error';
resultDiv.innerHTML = `<strong>❌ 错误</strong><br>${e.message}`;
}
});
function editUser(id) {
const user = users.find(u => u.id === id);
if (!user) return;
document.getElementById('userId').value = user.id;
document.getElementById('username').value = user.username;
document.getElementById('email').value = user.email;
document.getElementById('phone').value = user.phone || '';
document.getElementById('bio').value = user.bio || '';
document.getElementById('active').value = user.active.toString();
document.getElementById('formResult').innerHTML = '<span style="color:#11998e;">✏️ 正在编辑用户 #' + id + '</span>';
window.scrollTo(0, 0);
}
async function deleteUser(id) {
if (!confirm(`确定要删除用户 #${id} 吗?`)) return;
try {
const res = await fetch(`/api/users/${id}`, { method: 'DELETE' });
if (res.ok) {
alert('✅ 删除成功');
loadUsers();
} else {
alert('❌ 删除失败');
}
} catch (e) {
alert('删除失败: ' + e.message);
}
}
function resetForm() {
document.getElementById('userForm').reset();
document.getElementById('userId').value = '';
document.getElementById('formResult').innerHTML = '';
}
// 初始化
loadUsers();
</script>
</body>
</html>

View File

@@ -0,0 +1,3 @@
artifactId=springboot-scaffold
groupId=com.example
version=1.0.0

View File

@@ -0,0 +1,26 @@
com/example/scaffold/dto/OrderCreateRequest.class
com/example/scaffold/learning/MyBatisLearningController.class
com/example/scaffold/controller/HelloController.class
com/example/scaffold/service/UserService.class
com/example/scaffold/SpringbootScaffoldApplication.class
com/example/scaffold/service/impl/UserServiceImpl.class
com/example/scaffold/controller/UserController.class
com/example/scaffold/learning/IocLearningController.class
com/example/scaffold/aop/LearningAspect.class
com/example/scaffold/service/impl/OrderService.class
com/example/scaffold/controller/ProductOrderController.class
com/example/scaffold/mapper/ProductMapper.class
com/example/scaffold/mapper/OrderMapper.class
com/example/scaffold/aop/PerformanceAspect.class
com/example/scaffold/entity/User.class
com/example/scaffold/dto/UserCreateRequest.class
com/example/scaffold/dto/ProductCreateRequest.class
com/example/scaffold/entity/Order.class
com/example/scaffold/mapper/UserMapper.class
com/example/scaffold/learning/TransactionLearningController.class
com/example/scaffold/learning/IocLearningController$LearningBean.class
com/example/scaffold/controller/RootController.class
com/example/scaffold/learning/AopLearningController.class
com/example/scaffold/aop/PerformanceAspect$MethodStats.class
com/example/scaffold/config/AppConfig.class
com/example/scaffold/entity/Product.class

View File

@@ -0,0 +1,24 @@
/home/llm/Projects/springboot-scaffold/src/main/java/com/example/scaffold/mapper/ProductMapper.java
/home/llm/Projects/springboot-scaffold/src/main/java/com/example/scaffold/controller/UserController.java
/home/llm/Projects/springboot-scaffold/src/main/java/com/example/scaffold/learning/AopLearningController.java
/home/llm/Projects/springboot-scaffold/src/main/java/com/example/scaffold/entity/Product.java
/home/llm/Projects/springboot-scaffold/src/main/java/com/example/scaffold/entity/Order.java
/home/llm/Projects/springboot-scaffold/src/main/java/com/example/scaffold/mapper/UserMapper.java
/home/llm/Projects/springboot-scaffold/src/main/java/com/example/scaffold/dto/ProductCreateRequest.java
/home/llm/Projects/springboot-scaffold/src/main/java/com/example/scaffold/controller/HelloController.java
/home/llm/Projects/springboot-scaffold/src/main/java/com/example/scaffold/service/impl/UserServiceImpl.java
/home/llm/Projects/springboot-scaffold/src/main/java/com/example/scaffold/dto/OrderCreateRequest.java
/home/llm/Projects/springboot-scaffold/src/main/java/com/example/scaffold/learning/TransactionLearningController.java
/home/llm/Projects/springboot-scaffold/src/main/java/com/example/scaffold/aop/PerformanceAspect.java
/home/llm/Projects/springboot-scaffold/src/main/java/com/example/scaffold/entity/User.java
/home/llm/Projects/springboot-scaffold/src/main/java/com/example/scaffold/learning/IocLearningController.java
/home/llm/Projects/springboot-scaffold/src/main/java/com/example/scaffold/controller/ProductOrderController.java
/home/llm/Projects/springboot-scaffold/src/main/java/com/example/scaffold/mapper/OrderMapper.java
/home/llm/Projects/springboot-scaffold/src/main/java/com/example/scaffold/config/AppConfig.java
/home/llm/Projects/springboot-scaffold/src/main/java/com/example/scaffold/controller/RootController.java
/home/llm/Projects/springboot-scaffold/src/main/java/com/example/scaffold/service/UserService.java
/home/llm/Projects/springboot-scaffold/src/main/java/com/example/scaffold/learning/MyBatisLearningController.java
/home/llm/Projects/springboot-scaffold/src/main/java/com/example/scaffold/service/impl/OrderService.java
/home/llm/Projects/springboot-scaffold/src/main/java/com/example/scaffold/SpringbootScaffoldApplication.java
/home/llm/Projects/springboot-scaffold/src/main/java/com/example/scaffold/dto/UserCreateRequest.java
/home/llm/Projects/springboot-scaffold/src/main/java/com/example/scaffold/aop/LearningAspect.java

Binary file not shown.

Binary file not shown.