feat: Spring Boot 学习脚手架 v2.0
- 新增 IoC 容器学习模块 - 新增 AOP 切面编程学习模块 - 新增 MyBatis 集成学习模块 - 新增事务管理学习模块 - 新增用户/产品/订单 CRUD - 新增 7 个交互式学习页面 - 集成性能监控切面
This commit is contained in:
92
pom.xml
Normal file
92
pom.xml
Normal 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
193
springboot.log
Normal 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
|
||||||
|
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
115
src/main/java/com/example/scaffold/aop/LearningAspect.java
Normal file
115
src/main/java/com/example/scaffold/aop/LearningAspect.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
38
src/main/java/com/example/scaffold/config/AppConfig.java
Normal file
38
src/main/java/com/example/scaffold/config/AppConfig.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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!";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
42
src/main/java/com/example/scaffold/entity/Order.java
Normal file
42
src/main/java/com/example/scaffold/entity/Order.java
Normal 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;
|
||||||
|
}
|
||||||
42
src/main/java/com/example/scaffold/entity/Product.java
Normal file
42
src/main/java/com/example/scaffold/entity/Product.java
Normal 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;
|
||||||
|
}
|
||||||
101
src/main/java/com/example/scaffold/entity/User.java
Normal file
101
src/main/java/com/example/scaffold/entity/User.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 调用 B,B 加入 A 的事务"
|
||||||
|
),
|
||||||
|
"REQUIRES_NEW", Map.of(
|
||||||
|
"描述", "总是新建事务,挂起当前事务",
|
||||||
|
"场景", "日志记录、独立子任务",
|
||||||
|
"示例", "A 调用 B,B 在新事务执行,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"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
33
src/main/java/com/example/scaffold/mapper/OrderMapper.java
Normal file
33
src/main/java/com/example/scaffold/mapper/OrderMapper.java
Normal 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);
|
||||||
|
}
|
||||||
40
src/main/java/com/example/scaffold/mapper/ProductMapper.java
Normal file
40
src/main/java/com/example/scaffold/mapper/ProductMapper.java
Normal 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);
|
||||||
|
}
|
||||||
78
src/main/java/com/example/scaffold/mapper/UserMapper.java
Normal file
78
src/main/java/com/example/scaffold/mapper/UserMapper.java
Normal 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);
|
||||||
|
}
|
||||||
24
src/main/java/com/example/scaffold/service/UserService.java
Normal file
24
src/main/java/com/example/scaffold/service/UserService.java
Normal 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();
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
22
src/main/resources/application.properties
Normal file
22
src/main/resources/application.properties
Normal 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
|
||||||
159
src/main/resources/static/aop.html
Normal file
159
src/main/resources/static/aop.html
Normal 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>
|
||||||
280
src/main/resources/static/api.html
Normal file
280
src/main/resources/static/api.html
Normal 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>
|
||||||
203
src/main/resources/static/index.html
Normal file
203
src/main/resources/static/index.html
Normal 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>
|
||||||
175
src/main/resources/static/ioc.html
Normal file
175
src/main/resources/static/ioc.html
Normal 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>
|
||||||
176
src/main/resources/static/mybatis.html
Normal file
176
src/main/resources/static/mybatis.html
Normal 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"><select id="findUsers"><br> SELECT * FROM users<br> <where><br> <if test="name != null"><br> AND name LIKE CONCAT('%', #{name}, '%')<br> </if><br> <if test="email != null"><br> AND email = #{email}<br> </if><br> </where><br></select></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>
|
||||||
191
src/main/resources/static/transaction.html
Normal file
191
src/main/resources/static/transaction.html
Normal 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>
|
||||||
278
src/main/resources/static/users.html
Normal file
278
src/main/resources/static/users.html
Normal 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>
|
||||||
22
target/classes/application.properties
Normal file
22
target/classes/application.properties
Normal 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.
BIN
target/classes/com/example/scaffold/aop/LearningAspect.class
Normal file
BIN
target/classes/com/example/scaffold/aop/LearningAspect.class
Normal file
Binary file not shown.
Binary file not shown.
BIN
target/classes/com/example/scaffold/aop/PerformanceAspect.class
Normal file
BIN
target/classes/com/example/scaffold/aop/PerformanceAspect.class
Normal file
Binary file not shown.
BIN
target/classes/com/example/scaffold/config/AppConfig.class
Normal file
BIN
target/classes/com/example/scaffold/config/AppConfig.class
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
target/classes/com/example/scaffold/dto/OrderCreateRequest.class
Normal file
BIN
target/classes/com/example/scaffold/dto/OrderCreateRequest.class
Normal file
Binary file not shown.
Binary file not shown.
BIN
target/classes/com/example/scaffold/dto/UserCreateRequest.class
Normal file
BIN
target/classes/com/example/scaffold/dto/UserCreateRequest.class
Normal file
Binary file not shown.
BIN
target/classes/com/example/scaffold/entity/Order.class
Normal file
BIN
target/classes/com/example/scaffold/entity/Order.class
Normal file
Binary file not shown.
BIN
target/classes/com/example/scaffold/entity/Product.class
Normal file
BIN
target/classes/com/example/scaffold/entity/Product.class
Normal file
Binary file not shown.
BIN
target/classes/com/example/scaffold/entity/User.class
Normal file
BIN
target/classes/com/example/scaffold/entity/User.class
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
target/classes/com/example/scaffold/mapper/OrderMapper.class
Normal file
BIN
target/classes/com/example/scaffold/mapper/OrderMapper.class
Normal file
Binary file not shown.
BIN
target/classes/com/example/scaffold/mapper/ProductMapper.class
Normal file
BIN
target/classes/com/example/scaffold/mapper/ProductMapper.class
Normal file
Binary file not shown.
BIN
target/classes/com/example/scaffold/mapper/UserMapper.class
Normal file
BIN
target/classes/com/example/scaffold/mapper/UserMapper.class
Normal file
Binary file not shown.
BIN
target/classes/com/example/scaffold/service/UserService.class
Normal file
BIN
target/classes/com/example/scaffold/service/UserService.class
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
159
target/classes/static/aop.html
Normal file
159
target/classes/static/aop.html
Normal 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>
|
||||||
280
target/classes/static/api.html
Normal file
280
target/classes/static/api.html
Normal 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>
|
||||||
203
target/classes/static/index.html
Normal file
203
target/classes/static/index.html
Normal 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>
|
||||||
175
target/classes/static/ioc.html
Normal file
175
target/classes/static/ioc.html
Normal 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>
|
||||||
176
target/classes/static/mybatis.html
Normal file
176
target/classes/static/mybatis.html
Normal 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"><select id="findUsers"><br> SELECT * FROM users<br> <where><br> <if test="name != null"><br> AND name LIKE CONCAT('%', #{name}, '%')<br> </if><br> <if test="email != null"><br> AND email = #{email}<br> </if><br> </where><br></select></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>
|
||||||
191
target/classes/static/transaction.html
Normal file
191
target/classes/static/transaction.html
Normal 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>
|
||||||
278
target/classes/static/users.html
Normal file
278
target/classes/static/users.html
Normal 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>
|
||||||
3
target/maven-archiver/pom.properties
Normal file
3
target/maven-archiver/pom.properties
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
artifactId=springboot-scaffold
|
||||||
|
groupId=com.example
|
||||||
|
version=1.0.0
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
BIN
target/springboot-scaffold-1.0.0.jar
Normal file
BIN
target/springboot-scaffold-1.0.0.jar
Normal file
Binary file not shown.
BIN
target/springboot-scaffold-1.0.0.jar.original
Normal file
BIN
target/springboot-scaffold-1.0.0.jar.original
Normal file
Binary file not shown.
Reference in New Issue
Block a user