commit c04235c6550aff10a547a0dd11a8adc5a5908a6c Author: likingcode Date: Sat Mar 7 08:37:40 2026 +0000 feat: Spring Boot 学习脚手架 v2.0 - 新增 IoC 容器学习模块 - 新增 AOP 切面编程学习模块 - 新增 MyBatis 集成学习模块 - 新增事务管理学习模块 - 新增用户/产品/订单 CRUD - 新增 7 个交互式学习页面 - 集成性能监控切面 diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..3adb944 --- /dev/null +++ b/pom.xml @@ -0,0 +1,92 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.2.3 + + com.example + springboot-scaffold + 1.0.0 + + 17 + + + + + org.springframework.boot + spring-boot-starter-web + + + + + org.springframework.boot + spring-boot-starter-aop + + + + + org.springframework.boot + spring-boot-starter-validation + + + + + org.mybatis.spring.boot + mybatis-spring-boot-starter + 3.0.3 + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + + com.h2database + h2 + runtime + + + + + org.projectlombok + lombok + true + + + + + org.springframework.boot + spring-boot-starter-actuator + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + diff --git a/springboot.log b/springboot.log new file mode 100644 index 0000000..1fe4c98 --- /dev/null +++ b/springboot.log @@ -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.(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.(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.(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.(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.(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.(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.(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.(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 + diff --git a/src/main/java/com/example/scaffold/SpringbootScaffoldApplication.java b/src/main/java/com/example/scaffold/SpringbootScaffoldApplication.java new file mode 100644 index 0000000..17a5b94 --- /dev/null +++ b/src/main/java/com/example/scaffold/SpringbootScaffoldApplication.java @@ -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); + } +} diff --git a/src/main/java/com/example/scaffold/aop/LearningAspect.java b/src/main/java/com/example/scaffold/aop/LearningAspect.java new file mode 100644 index 0000000..af193f3 --- /dev/null +++ b/src/main/java/com/example/scaffold/aop/LearningAspect.java @@ -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; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/scaffold/aop/PerformanceAspect.java b/src/main/java/com/example/scaffold/aop/PerformanceAspect.java new file mode 100644 index 0000000..66f8bf6 --- /dev/null +++ b/src/main/java/com/example/scaffold/aop/PerformanceAspect.java @@ -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 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 getStats() { + return new ConcurrentHashMap<>(statsMap); + } + + public void resetStats() { + statsMap.clear(); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/scaffold/config/AppConfig.java b/src/main/java/com/example/scaffold/config/AppConfig.java new file mode 100644 index 0000000..aa79413 --- /dev/null +++ b/src/main/java/com/example/scaffold/config/AppConfig.java @@ -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); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/scaffold/controller/HelloController.java b/src/main/java/com/example/scaffold/controller/HelloController.java new file mode 100644 index 0000000..7246961 --- /dev/null +++ b/src/main/java/com/example/scaffold/controller/HelloController.java @@ -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"; + } +} diff --git a/src/main/java/com/example/scaffold/controller/ProductOrderController.java b/src/main/java/com/example/scaffold/controller/ProductOrderController.java new file mode 100644 index 0000000..3eba241 --- /dev/null +++ b/src/main/java/com/example/scaffold/controller/ProductOrderController.java @@ -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 listProducts() { + return productMapper.findAll(); + } + + @GetMapping("/products/{id}") + public ResponseEntity getProduct(@PathVariable Long id) { + Product product = productMapper.findById(id); + return product != null ? ResponseEntity.ok(product) : ResponseEntity.notFound().build(); + } + + @GetMapping("/products/category/{category}") + public List getByCategory(@PathVariable String category) { + return productMapper.findByCategory(category); + } + + @GetMapping("/products/price") + public List getByPriceRange(@RequestParam BigDecimal min, @RequestParam BigDecimal max) { + return productMapper.findByPriceRange(min, max); + } + + @PostMapping("/products") + public ResponseEntity 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 deleteProduct(@PathVariable Long id) { + productMapper.deleteById(id); + return ResponseEntity.noContent().build(); + } + + // ==================== 订单 API ==================== + + @GetMapping("/orders") + public List listOrders() { + return orderService.findAll(); + } + + @GetMapping("/orders/{id}") + public ResponseEntity getOrder(@PathVariable Long id) { + Order order = orderService.getOrder(id); + return order != null ? ResponseEntity.ok(order) : ResponseEntity.notFound().build(); + } + + @GetMapping("/orders/user/{userId}") + public List 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 updateOrderStatus(@PathVariable Long id, @RequestParam String status) { + orderService.updateStatus(id, status); + return ResponseEntity.ok().build(); + } + + @DeleteMapping("/orders/{id}") + public ResponseEntity deleteOrder(@PathVariable Long id) { + orderService.deleteById(id); + return ResponseEntity.noContent().build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/scaffold/controller/RootController.java b/src/main/java/com/example/scaffold/controller/RootController.java new file mode 100644 index 0000000..bec2d6f --- /dev/null +++ b/src/main/java/com/example/scaffold/controller/RootController.java @@ -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!"; + } +} diff --git a/src/main/java/com/example/scaffold/controller/UserController.java b/src/main/java/com/example/scaffold/controller/UserController.java new file mode 100644 index 0000000..7deed58 --- /dev/null +++ b/src/main/java/com/example/scaffold/controller/UserController.java @@ -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 list() { + return userService.findAll(); + } + + /** + * GET - 根据 ID 查询 + */ + @GetMapping("/{id}") + public ResponseEntity getById(@PathVariable Long id) { + return userService.findById(id) + .map(ResponseEntity::ok) + .orElse(ResponseEntity.notFound().build()); + } + + /** + * GET - 搜索用户 + */ + @GetMapping("/search") + public List search(@RequestParam String username) { + return userService.searchByUsername(username); + } + + /** + * POST - 创建用户 + */ + @PostMapping + public ResponseEntity 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 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 delete(@PathVariable Long id) { + userService.deleteById(id); + return ResponseEntity.noContent().build(); + } + + /** + * GET - 统计 + */ + @GetMapping("/count") + public Map count() { + return Map.of("count", userService.count()); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/scaffold/dto/OrderCreateRequest.java b/src/main/java/com/example/scaffold/dto/OrderCreateRequest.java new file mode 100644 index 0000000..e04c9de --- /dev/null +++ b/src/main/java/com/example/scaffold/dto/OrderCreateRequest.java @@ -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; +} \ No newline at end of file diff --git a/src/main/java/com/example/scaffold/dto/ProductCreateRequest.java b/src/main/java/com/example/scaffold/dto/ProductCreateRequest.java new file mode 100644 index 0000000..3f38d59 --- /dev/null +++ b/src/main/java/com/example/scaffold/dto/ProductCreateRequest.java @@ -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; +} \ No newline at end of file diff --git a/src/main/java/com/example/scaffold/dto/UserCreateRequest.java b/src/main/java/com/example/scaffold/dto/UserCreateRequest.java new file mode 100644 index 0000000..8ad9a9f --- /dev/null +++ b/src/main/java/com/example/scaffold/dto/UserCreateRequest.java @@ -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; +} \ No newline at end of file diff --git a/src/main/java/com/example/scaffold/entity/Order.java b/src/main/java/com/example/scaffold/entity/Order.java new file mode 100644 index 0000000..5127372 --- /dev/null +++ b/src/main/java/com/example/scaffold/entity/Order.java @@ -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; +} \ No newline at end of file diff --git a/src/main/java/com/example/scaffold/entity/Product.java b/src/main/java/com/example/scaffold/entity/Product.java new file mode 100644 index 0000000..b568c64 --- /dev/null +++ b/src/main/java/com/example/scaffold/entity/Product.java @@ -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; +} \ No newline at end of file diff --git a/src/main/java/com/example/scaffold/entity/User.java b/src/main/java/com/example/scaffold/entity/User.java new file mode 100644 index 0000000..5389caa --- /dev/null +++ b/src/main/java/com/example/scaffold/entity/User.java @@ -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); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/scaffold/learning/AopLearningController.java b/src/main/java/com/example/scaffold/learning/AopLearningController.java new file mode 100644 index 0000000..e5b06ea --- /dev/null +++ b/src/main/java/com/example/scaffold/learning/AopLearningController.java @@ -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 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 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 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 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" + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/scaffold/learning/IocLearningController.java b/src/main/java/com/example/scaffold/learning/IocLearningController.java new file mode 100644 index 0000000..345acd5 --- /dev/null +++ b/src/main/java/com/example/scaffold/learning/IocLearningController.java @@ -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 listBeans() { + String[] beanNames = applicationContext.getBeanDefinitionNames(); + + Map 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 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 injectionTypes() { + return Map.of( + "构造器注入", "推荐!明确依赖,不可变,易于测试", + "Setter注入", "可选依赖,灵活性高", + "字段注入", "不推荐!隐藏依赖,难以测试", + "本控制器使用", "构造器注入 (RequiredArgsConstructor) + 字段注入演示" + ); + } + + /** + * 演示 Bean 作用域 + */ + @GetMapping("/scopes") + public Map scopes() { + return Map.of( + "singleton", "单例 - 整个应用只有一个实例(默认)", + "prototype", "原型 - 每次请求都创建新实例", + "request", "请求 - 每个 HTTP 请求一个实例", + "session", "会话 - 每个 HTTP 会话一个实例", + "demo", learningBean.getInstanceInfo() + ); + } + + /** + * 性能统计 + */ + @GetMapping("/performance") + public Map getPerformance() { + var stats = performanceAspect.getStats(); + Map 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 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); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/scaffold/learning/MyBatisLearningController.java b/src/main/java/com/example/scaffold/learning/MyBatisLearningController.java new file mode 100644 index 0000000..ee3e65a --- /dev/null +++ b/src/main/java/com/example/scaffold/learning/MyBatisLearningController.java @@ -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 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 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 dynamicSql() { + return Map.of( + "if", "条件判断 AND name = #{name} ", + "choose/when/otherwise", "类似 switch-case", + "trim/where/set", "处理 SQL 拼接", + "foreach", "循环遍历 #{item}", + "bind", "定义变量 ", + "示例", """ + + """ + ); + } + + /** + * 缓存演示 + */ + @GetMapping("/cache") + public Map 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 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() + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/scaffold/learning/TransactionLearningController.java b/src/main/java/com/example/scaffold/learning/TransactionLearningController.java new file mode 100644 index 0000000..be608f7 --- /dev/null +++ b/src/main/java/com/example/scaffold/learning/TransactionLearningController.java @@ -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 concepts() { + return Map.of( + "ACID", Map.of( + "Atomicity (原子性)", "事务是不可分割的工作单位", + "Consistency (一致性)", "事务必须使数据库从一个一致性状态变换到另一个一致性状态", + "Isolation (隔离性)", "多个用户并发访问数据库时,数据库为每个用户开启的事务不能被其他事务干扰", + "Durability (持久性)", "事务一旦提交,对数据库的改变是永久性的" + ), + "Spring事务", "@Transactional 注解实现声明式事务管理" + ); + } + + /** + * 传播行为详解 + */ + @GetMapping("/propagation") + public Map 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 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 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" + ) + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/scaffold/mapper/OrderMapper.java b/src/main/java/com/example/scaffold/mapper/OrderMapper.java new file mode 100644 index 0000000..4206f36 --- /dev/null +++ b/src/main/java/com/example/scaffold/mapper/OrderMapper.java @@ -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 findAll(); + + @Select("SELECT * FROM orders WHERE id = #{id}") + Order findById(@Param("id") Long id); + + @Select("SELECT * FROM orders WHERE user_id = #{userId}") + List 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); +} \ No newline at end of file diff --git a/src/main/java/com/example/scaffold/mapper/ProductMapper.java b/src/main/java/com/example/scaffold/mapper/ProductMapper.java new file mode 100644 index 0000000..768d08a --- /dev/null +++ b/src/main/java/com/example/scaffold/mapper/ProductMapper.java @@ -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 findAll(); + + @Select("SELECT * FROM products WHERE id = #{id}") + Product findById(@Param("id") Long id); + + @Select("SELECT * FROM products WHERE category = #{category}") + List findByCategory(@Param("category") String category); + + @Select("SELECT * FROM products WHERE price BETWEEN #{min} AND #{max} ORDER BY price") + List 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); +} \ No newline at end of file diff --git a/src/main/java/com/example/scaffold/mapper/UserMapper.java b/src/main/java/com/example/scaffold/mapper/UserMapper.java new file mode 100644 index 0000000..49d3cfa --- /dev/null +++ b/src/main/java/com/example/scaffold/mapper/UserMapper.java @@ -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 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 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("") + int batchInsert(@Param("users") List users); +} \ No newline at end of file diff --git a/src/main/java/com/example/scaffold/service/UserService.java b/src/main/java/com/example/scaffold/service/UserService.java new file mode 100644 index 0000000..7401f18 --- /dev/null +++ b/src/main/java/com/example/scaffold/service/UserService.java @@ -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 findAll(); + + Optional findById(Long id); + + User save(User user); + + void deleteById(Long id); + + List searchByUsername(String username); + + long count(); +} \ No newline at end of file diff --git a/src/main/java/com/example/scaffold/service/impl/OrderService.java b/src/main/java/com/example/scaffold/service/impl/OrderService.java new file mode 100644 index 0000000..de05fa3 --- /dev/null +++ b/src/main/java/com/example/scaffold/service/impl/OrderService.java @@ -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 findAll() { + return orderMapper.findAll(); + } + + public List 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); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/scaffold/service/impl/UserServiceImpl.java b/src/main/java/com/example/scaffold/service/impl/UserServiceImpl.java new file mode 100644 index 0000000..2e7f44a --- /dev/null +++ b/src/main/java/com/example/scaffold/service/impl/UserServiceImpl.java @@ -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 findAll() { + log.info("📊 [UserService] 查询所有用户"); + return userMapper.findAll(); + } + + @Override + public Optional 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 searchByUsername(String username) { + log.info("🔎 [UserService] 搜索用户: username={}", username); + return userMapper.findByUsernameLike(username); + } + + @Override + public long count() { + return userMapper.count(); + } +} \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..21224e8 --- /dev/null +++ b/src/main/resources/application.properties @@ -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 \ No newline at end of file diff --git a/src/main/resources/static/aop.html b/src/main/resources/static/aop.html new file mode 100644 index 0000000..2773d00 --- /dev/null +++ b/src/main/resources/static/aop.html @@ -0,0 +1,159 @@ + + + + + + AOP 切面学习 - Spring Boot + + + +
+
+

🔪 AOP 切面编程

+

Aspect Oriented Programming - 面向切面编程

+
+ + + +
+

📚 核心概念

+

AOP (面向切面编程):将横切关注点(日志、权限、事务等)从业务逻辑中分离出来,实现模块化。

+
+
目标方法
+
+
@Before
+
+
@Around (前)
+
+
方法执行
+
+
@Around (后)
+
+
@AfterReturning
+
+
@After
+
+
+ +
+

🔔 五种通知类型

+ +
+

@Before - 前置通知

+

方法执行前触发,可用于参数校验、日志记录

+
@Before("execution(* com.example.service..*.*(..))")
public void before(JoinPoint jp) {
log.info("即将执行: " + jp.getSignature().getName());
}
+
+ +
+

@After - 后置通知

+

方法执行后触发(无论是否异常),可用于资源释放

+
@After("execution(* com.example.service..*.*(..))")
public void after(JoinPoint jp) {
log.info("执行完成: " + jp.getSignature().getName());
}
+
+ +
+

@AfterReturning - 返回通知

+

方法成功返回后触发,可获取返回值

+
@AfterReturning(pointcut="...", returning="result")
public void afterReturning(Object result) {
log.info("返回结果: " + result);
}
+
+ +
+

@AfterThrowing - 异常通知

+

方法抛出异常后触发,可用于异常处理

+
@AfterThrowing(pointcut="...", throwing="ex")
public void afterThrowing(Exception ex) {
log.error("发生异常: " + ex.getMessage());
}
+
+ +
+

@Around - 环绕通知

+

完全控制方法执行,可决定是否执行目标方法

+
@Around("execution(* com.example.controller..*.*(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
Object result = pjp.proceed(); // 执行目标方法
long cost = System.currentTimeMillis() - start;
log.info("耗时: " + cost + "ms");
return result;
}
+
+
+ +
+

🧪 在线测试

+

调用下面的 API,然后查看控制台日志观察 AOP 执行顺序

+ + +
+
+ +
+

📝 切入点表达式

+ + + + + + + +
表达式含义
execution(* *(..))匹配所有方法
execution(* com.example.service.*.*(..))service包下所有方法
execution(* com.example.service..*.*(..))service包及子包所有方法
execution(public * *(..))所有public方法
@annotation(org.springframework.transaction.annotation.Transactional)带@Transactional的方法
+
+
+ + + + \ No newline at end of file diff --git a/src/main/resources/static/api.html b/src/main/resources/static/api.html new file mode 100644 index 0000000..d801914 --- /dev/null +++ b/src/main/resources/static/api.html @@ -0,0 +1,280 @@ + + + + + + API 测试面板 - Spring Boot + + + +
+
+

🔌 API 测试面板

+

在线测试所有 RESTful API

+
+ + + +
+
👥 用户 API
+
📦 产品 API
+
🛒 订单 API
+
📚 学习 API
+
+ + +
+
+

用户管理 API

+
+
+ GET + 获取所有用户 +
/api/users
+ +
+
+ +
+ GET + 获取单个用户 +
/api/users/{id}
+ + +
+
+ +
+ POST + 创建用户 +
/api/users
+ + +
+
+ +
+ GET + 搜索用户 +
/api/users/search?username={name}
+ + +
+
+
+
+
+ + +
+
+

产品管理 API

+
+
+ GET + 获取所有产品 +
/api/products
+ +
+
+ +
+ POST + 创建产品 +
/api/products
+ + +
+
+
+
+
+ + +
+
+

订单管理 API (演示事务)

+
+
+ GET + 获取所有订单 +
/api/orders
+ +
+
+ +
+ POST + 创建订单 +
/api/orders
+ + +
+
+ +
+ POST + 创建订单(触发回滚) +
/api/orders (rollback=true)
+ + +
+
+
+
+
+ + +
+
+

学习 API

+
+
+ GET + IoC - 查看所有 Bean +
/api/learning/ioc/beans
+ +
+
+ +
+ GET + AOP - 概念 +
/api/learning/aop/concepts
+ +
+
+ +
+ GET + MyBatis - 配置 +
/api/learning/mybatis/config
+ +
+
+ +
+ GET + 事务 - 传播行为 +
/api/learning/transaction/propagation
+ +
+
+
+
+
+
+ + + + \ No newline at end of file diff --git a/src/main/resources/static/index.html b/src/main/resources/static/index.html new file mode 100644 index 0000000..d137181 --- /dev/null +++ b/src/main/resources/static/index.html @@ -0,0 +1,203 @@ + + + + + + Spring Boot 学习中心 + + + +
+
+

🍃 Spring Boot 学习中心

+

交互式学习 Spring 核心功能 | IoC · AOP · MyBatis · 事务

+
+ +
+
+
-
+
已加载 Bean
+
+
+
-
+
用户数量
+
+
+
-
+
产品数量
+
+
+
-
+
订单数量
+
+
+ + + +
+
+

📦 IoC 容器

+

理解 Spring 的核心:控制反转和依赖注入。学习 Bean 的生命周期、作用域和各种注入方式。

+
    +
  • Bean 生命周期演示
  • +
  • 依赖注入方式对比
  • +
  • Bean 作用域详解
  • +
  • 性能统计面板
  • +
+ 开始学习 → +
+ +
+

🔪 AOP 切面编程

+

掌握面向切面编程,实现日志、性能监控、事务等横切关注点的模块化管理。

+
    +
  • 5 种通知类型演示
  • +
  • 切入点表达式语法
  • +
  • 实时日志展示
  • +
  • 性能监控切面
  • +
+ 开始学习 → +
+ +
+

💾 MyBatis 集成

+

学习 MyBatis 与 Spring Boot 的整合,对比 JPA,掌握动态 SQL 和缓存机制。

+
    +
  • MyBatis vs JPA 对比
  • +
  • 动态 SQL 语法
  • +
  • 一级/二级缓存演示
  • +
  • 批量操作示例
  • +
+ 开始学习 → +
+ +
+

🔄 事务管理

+

深入理解 Spring 声明式事务,掌握传播行为和隔离级别的实际应用。

+
    +
  • 事务传播行为
  • +
  • 事务隔离级别
  • +
  • 回滚机制演示
  • +
  • 订单创建场景
  • +
+ 开始学习 → +
+ +
+

👥 用户管理 CRUD

+

完整的 RESTful API 示例,演示增删改查操作和参数验证。

+
    +
  • RESTful 设计规范
  • +
  • 参数验证
  • +
  • 异常处理
  • +
  • 交互式测试
  • +
+ 开始学习 → +
+ +
+

🔌 API 测试面板

+

在线测试所有 API 接口,查看请求响应,理解 RESTful API 工作原理。

+
    +
  • 用户 API
  • +
  • 产品 API
  • +
  • 订单 API
  • +
  • 学习 API
  • +
+ 开始测试 → +
+
+ +
+

🚀 快速开始 - API 示例

+
+GET  /api/users           # 获取所有用户
+GET  /api/users/{id}       # 获取单个用户
+POST /api/users           # 创建用户
+PUT  /api/users/{id}       # 更新用户
+DEL  /api/users/{id}       # 删除用户
+
+GET  /api/learning/ioc/beans      # 查看所有 Bean
+GET  /api/learning/aop/concepts   # AOP 概念
+GET  /api/learning/mybatis/cache  # 缓存机制
+GET  /api/learning/transaction/propagation  # 传播行为
+            
+
+ +
+

🍃 Spring Boot 学习脚手架 | H2 控制台 (JDBC: jdbc:h2:file:~/h2/springboot_scaffold, 用户: sa)

+
+
+ + + + \ No newline at end of file diff --git a/src/main/resources/static/ioc.html b/src/main/resources/static/ioc.html new file mode 100644 index 0000000..473f3f1 --- /dev/null +++ b/src/main/resources/static/ioc.html @@ -0,0 +1,175 @@ + + + + + + IoC 容器学习 - Spring Boot + + + +
+
+

📦 IoC 容器学习

+

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

+
+ + + +
+

📚 核心概念

+
+

什么是 IoC?

+

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

+

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

+
+
+

为什么用 IoC?

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

🔍 查看所有 Bean

+

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

+ + +
+
+ +
+

📊 Bean 作用域

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

⚡ 性能统计

+

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

+ + +
+
+ +
+

💉 依赖注入方式对比

+ + + + + +
方式优点缺点推荐度
构造器注入明确依赖、不可变、易测试参数多时代码长⭐⭐⭐⭐⭐
Setter 注入可选依赖、灵活可能为 null⭐⭐⭐
字段注入代码简洁隐藏依赖、难测试
+
+
+ + + + \ No newline at end of file diff --git a/src/main/resources/static/mybatis.html b/src/main/resources/static/mybatis.html new file mode 100644 index 0000000..f4b7d19 --- /dev/null +++ b/src/main/resources/static/mybatis.html @@ -0,0 +1,176 @@ + + + + + + MyBatis 学习 - Spring Boot + + + +
+
+

💾 MyBatis 学习

+

半自动 ORM 框架 - SQL 与 Java 对象的映射

+
+ + + +
+

📊 MyBatis vs JPA 对比

+
+
+

MyBatis

+

优点:

+
    +
  • ✅ SQL 灵活可控
  • +
  • ✅ 性能优化方便
  • +
  • ✅ 复杂查询友好
  • +
  • ✅ 易于 DBA 协作
  • +
+

缺点:

+
    +
  • ❌ SQL 与代码耦合
  • +
  • ❌ 数据库迁移成本高
  • +
  • ❌ 需要手写 SQL
  • +
+

适用:复杂查询、性能要求高

+
+
+

JPA/Hibernate

+

优点:

+
    +
  • ✅ 面向对象
  • +
  • ✅ 数据库无关
  • +
  • ✅ 开发效率高
  • +
  • ✅ 自动维护
  • +
+

缺点:

+
    +
  • ❌ 复杂查询困难
  • +
  • ❌ 性能调优复杂
  • +
  • ❌ 学习曲线陡
  • +
+

适用:标准 CRUD、快速开发

+
+
+
+ +
+

🚀 快速体验

+ + + +
+
+ +
+

💡 缓存机制

+
+

一级缓存 (SqlSession 级别)

+

默认开启,同一 SqlSession 内相同查询只执行一次 SQL

+
// 第一次查询 - 执行 SQL
User u1 = mapper.findById(1L);
// 第二次查询 - 命中缓存,不执行 SQL
User u2 = mapper.findById(1L);
+
+
+

二级缓存 (Mapper 级别)

+

需配置 @CacheNamespace,多个 SqlSession 共享

+
@Mapper
@CacheNamespace
public interface UserMapper { ... }
+
+

💡 验证方式:查看控制台 SQL 日志,缓存命中时不打印 SQL

+
+ +
+

📝 常用注解

+ + + + + + + + + + +
注解用途
@Mapper标记为 MyBatis Mapper 接口
@Select查询 SQL
@Insert插入 SQL
@Update更新 SQL
@Delete删除 SQL
@Param参数命名
@Options额外选项(如返回自增ID)
@CacheNamespace启用二级缓存
+
+ +
+

🔧 动态 SQL 示例

+
<select id="findUsers">
SELECT * FROM users
<where>
<if test="name != null">
AND name LIKE CONCAT('%', #{name}, '%')
</if>
<if test="email != null">
AND email = #{email}
</if>
</where>
</select>
+
+
+ + + + \ No newline at end of file diff --git a/src/main/resources/static/transaction.html b/src/main/resources/static/transaction.html new file mode 100644 index 0000000..9ce3f5f --- /dev/null +++ b/src/main/resources/static/transaction.html @@ -0,0 +1,191 @@ + + + + + + 事务管理学习 - Spring Boot + + + +
+
+

🔄 事务管理

+

声明式事务 - @Transactional

+
+ + + +
+

📚 ACID 特性

+
+
+

A

+

Atomicity

+

原子性

+

事务是不可分割的工作单位

+
+
+

C

+

Consistency

+

一致性

+

数据库状态保持一致

+
+
+

I

+

Isolation

+

隔离性

+

事务之间相互隔离

+
+
+

D

+

Durability

+

持久性

+

提交后永久保存

+
+
+
+ +
+

🚀 传播行为 (Propagation)

+
+
+

REQUIRED (默认)

+

有事务则加入,无则新建

+

最常用,适合大多数业务方法

+
+
+

REQUIRES_NEW

+

总是新建事务,挂起当前事务

+

适合日志记录、独立子任务

+
+
+

SUPPORTS

+

有事务则加入,无则以非事务运行

+

适合查询方法

+
+
+

NOT_SUPPORTED

+

以非事务运行,挂起当前事务

+

不需要事务的操作

+
+
+

MANDATORY

+

必须在事务中运行,否则抛异常

+

强制要求事务

+
+
+

NEVER

+

不能在事务中运行,否则抛异常

+

确保无事务

+
+
+ +
+
+ +
+

🔒 隔离级别 (Isolation)

+ + + + + + +
级别脏读不可重复读幻读说明
READ_UNCOMMITTED读未提交,性能最高
READ_COMMITTED读已提交,Oracle默认
REPEATABLE_READ可重复读,MySQL默认
SERIALIZABLE串行化,性能最低
+

✅ = 防止该问题 | ❌ = 可能出现该问题

+ +
+
+ +
+

🧪 事务回滚演示

+

创建订单时触发异常,观察事务回滚效果

+ +
+
+
+ + + + \ No newline at end of file diff --git a/src/main/resources/static/users.html b/src/main/resources/static/users.html new file mode 100644 index 0000000..bdde258 --- /dev/null +++ b/src/main/resources/static/users.html @@ -0,0 +1,278 @@ + + + + + + 用户管理 - Spring Boot CRUD + + + +
+
+

👥 用户管理

+

RESTful CRUD 操作演示

+
+ + + +
+

➕ 创建/编辑用户

+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+ + +
+
+
+ +
+

📋 用户列表

+ + + + + + + + + + + + + + + + +
ID用户名邮箱手机号状态创建时间操作
加载中...
+
+
+ + + + \ No newline at end of file diff --git a/target/classes/application.properties b/target/classes/application.properties new file mode 100644 index 0000000..21224e8 --- /dev/null +++ b/target/classes/application.properties @@ -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 \ No newline at end of file diff --git a/target/classes/com/example/scaffold/SpringbootScaffoldApplication.class b/target/classes/com/example/scaffold/SpringbootScaffoldApplication.class new file mode 100644 index 0000000..9e57276 Binary files /dev/null and b/target/classes/com/example/scaffold/SpringbootScaffoldApplication.class differ diff --git a/target/classes/com/example/scaffold/aop/LearningAspect.class b/target/classes/com/example/scaffold/aop/LearningAspect.class new file mode 100644 index 0000000..4d4f534 Binary files /dev/null and b/target/classes/com/example/scaffold/aop/LearningAspect.class differ diff --git a/target/classes/com/example/scaffold/aop/PerformanceAspect$MethodStats.class b/target/classes/com/example/scaffold/aop/PerformanceAspect$MethodStats.class new file mode 100644 index 0000000..2f41c72 Binary files /dev/null and b/target/classes/com/example/scaffold/aop/PerformanceAspect$MethodStats.class differ diff --git a/target/classes/com/example/scaffold/aop/PerformanceAspect.class b/target/classes/com/example/scaffold/aop/PerformanceAspect.class new file mode 100644 index 0000000..ca9396d Binary files /dev/null and b/target/classes/com/example/scaffold/aop/PerformanceAspect.class differ diff --git a/target/classes/com/example/scaffold/config/AppConfig.class b/target/classes/com/example/scaffold/config/AppConfig.class new file mode 100644 index 0000000..5090439 Binary files /dev/null and b/target/classes/com/example/scaffold/config/AppConfig.class differ diff --git a/target/classes/com/example/scaffold/controller/HelloController.class b/target/classes/com/example/scaffold/controller/HelloController.class new file mode 100644 index 0000000..824dd4b Binary files /dev/null and b/target/classes/com/example/scaffold/controller/HelloController.class differ diff --git a/target/classes/com/example/scaffold/controller/ProductOrderController.class b/target/classes/com/example/scaffold/controller/ProductOrderController.class new file mode 100644 index 0000000..b438e7c Binary files /dev/null and b/target/classes/com/example/scaffold/controller/ProductOrderController.class differ diff --git a/target/classes/com/example/scaffold/controller/RootController.class b/target/classes/com/example/scaffold/controller/RootController.class new file mode 100644 index 0000000..7737341 Binary files /dev/null and b/target/classes/com/example/scaffold/controller/RootController.class differ diff --git a/target/classes/com/example/scaffold/controller/UserController.class b/target/classes/com/example/scaffold/controller/UserController.class new file mode 100644 index 0000000..bb4d108 Binary files /dev/null and b/target/classes/com/example/scaffold/controller/UserController.class differ diff --git a/target/classes/com/example/scaffold/dto/OrderCreateRequest.class b/target/classes/com/example/scaffold/dto/OrderCreateRequest.class new file mode 100644 index 0000000..4f2e329 Binary files /dev/null and b/target/classes/com/example/scaffold/dto/OrderCreateRequest.class differ diff --git a/target/classes/com/example/scaffold/dto/ProductCreateRequest.class b/target/classes/com/example/scaffold/dto/ProductCreateRequest.class new file mode 100644 index 0000000..1824753 Binary files /dev/null and b/target/classes/com/example/scaffold/dto/ProductCreateRequest.class differ diff --git a/target/classes/com/example/scaffold/dto/UserCreateRequest.class b/target/classes/com/example/scaffold/dto/UserCreateRequest.class new file mode 100644 index 0000000..11aff66 Binary files /dev/null and b/target/classes/com/example/scaffold/dto/UserCreateRequest.class differ diff --git a/target/classes/com/example/scaffold/entity/Order.class b/target/classes/com/example/scaffold/entity/Order.class new file mode 100644 index 0000000..033237a Binary files /dev/null and b/target/classes/com/example/scaffold/entity/Order.class differ diff --git a/target/classes/com/example/scaffold/entity/Product.class b/target/classes/com/example/scaffold/entity/Product.class new file mode 100644 index 0000000..c82395b Binary files /dev/null and b/target/classes/com/example/scaffold/entity/Product.class differ diff --git a/target/classes/com/example/scaffold/entity/User.class b/target/classes/com/example/scaffold/entity/User.class new file mode 100644 index 0000000..4506557 Binary files /dev/null and b/target/classes/com/example/scaffold/entity/User.class differ diff --git a/target/classes/com/example/scaffold/learning/AopLearningController.class b/target/classes/com/example/scaffold/learning/AopLearningController.class new file mode 100644 index 0000000..45931fa Binary files /dev/null and b/target/classes/com/example/scaffold/learning/AopLearningController.class differ diff --git a/target/classes/com/example/scaffold/learning/IocLearningController$LearningBean.class b/target/classes/com/example/scaffold/learning/IocLearningController$LearningBean.class new file mode 100644 index 0000000..c4dbc5c Binary files /dev/null and b/target/classes/com/example/scaffold/learning/IocLearningController$LearningBean.class differ diff --git a/target/classes/com/example/scaffold/learning/IocLearningController.class b/target/classes/com/example/scaffold/learning/IocLearningController.class new file mode 100644 index 0000000..561b785 Binary files /dev/null and b/target/classes/com/example/scaffold/learning/IocLearningController.class differ diff --git a/target/classes/com/example/scaffold/learning/MyBatisLearningController.class b/target/classes/com/example/scaffold/learning/MyBatisLearningController.class new file mode 100644 index 0000000..f389176 Binary files /dev/null and b/target/classes/com/example/scaffold/learning/MyBatisLearningController.class differ diff --git a/target/classes/com/example/scaffold/learning/TransactionLearningController.class b/target/classes/com/example/scaffold/learning/TransactionLearningController.class new file mode 100644 index 0000000..0d3edaf Binary files /dev/null and b/target/classes/com/example/scaffold/learning/TransactionLearningController.class differ diff --git a/target/classes/com/example/scaffold/mapper/OrderMapper.class b/target/classes/com/example/scaffold/mapper/OrderMapper.class new file mode 100644 index 0000000..a841855 Binary files /dev/null and b/target/classes/com/example/scaffold/mapper/OrderMapper.class differ diff --git a/target/classes/com/example/scaffold/mapper/ProductMapper.class b/target/classes/com/example/scaffold/mapper/ProductMapper.class new file mode 100644 index 0000000..bba1325 Binary files /dev/null and b/target/classes/com/example/scaffold/mapper/ProductMapper.class differ diff --git a/target/classes/com/example/scaffold/mapper/UserMapper.class b/target/classes/com/example/scaffold/mapper/UserMapper.class new file mode 100644 index 0000000..18d1c09 Binary files /dev/null and b/target/classes/com/example/scaffold/mapper/UserMapper.class differ diff --git a/target/classes/com/example/scaffold/service/UserService.class b/target/classes/com/example/scaffold/service/UserService.class new file mode 100644 index 0000000..827d5ab Binary files /dev/null and b/target/classes/com/example/scaffold/service/UserService.class differ diff --git a/target/classes/com/example/scaffold/service/impl/OrderService.class b/target/classes/com/example/scaffold/service/impl/OrderService.class new file mode 100644 index 0000000..d078843 Binary files /dev/null and b/target/classes/com/example/scaffold/service/impl/OrderService.class differ diff --git a/target/classes/com/example/scaffold/service/impl/UserServiceImpl.class b/target/classes/com/example/scaffold/service/impl/UserServiceImpl.class new file mode 100644 index 0000000..557c419 Binary files /dev/null and b/target/classes/com/example/scaffold/service/impl/UserServiceImpl.class differ diff --git a/target/classes/static/aop.html b/target/classes/static/aop.html new file mode 100644 index 0000000..2773d00 --- /dev/null +++ b/target/classes/static/aop.html @@ -0,0 +1,159 @@ + + + + + + AOP 切面学习 - Spring Boot + + + +
+
+

🔪 AOP 切面编程

+

Aspect Oriented Programming - 面向切面编程

+
+ + + +
+

📚 核心概念

+

AOP (面向切面编程):将横切关注点(日志、权限、事务等)从业务逻辑中分离出来,实现模块化。

+
+
目标方法
+
+
@Before
+
+
@Around (前)
+
+
方法执行
+
+
@Around (后)
+
+
@AfterReturning
+
+
@After
+
+
+ +
+

🔔 五种通知类型

+ +
+

@Before - 前置通知

+

方法执行前触发,可用于参数校验、日志记录

+
@Before("execution(* com.example.service..*.*(..))")
public void before(JoinPoint jp) {
log.info("即将执行: " + jp.getSignature().getName());
}
+
+ +
+

@After - 后置通知

+

方法执行后触发(无论是否异常),可用于资源释放

+
@After("execution(* com.example.service..*.*(..))")
public void after(JoinPoint jp) {
log.info("执行完成: " + jp.getSignature().getName());
}
+
+ +
+

@AfterReturning - 返回通知

+

方法成功返回后触发,可获取返回值

+
@AfterReturning(pointcut="...", returning="result")
public void afterReturning(Object result) {
log.info("返回结果: " + result);
}
+
+ +
+

@AfterThrowing - 异常通知

+

方法抛出异常后触发,可用于异常处理

+
@AfterThrowing(pointcut="...", throwing="ex")
public void afterThrowing(Exception ex) {
log.error("发生异常: " + ex.getMessage());
}
+
+ +
+

@Around - 环绕通知

+

完全控制方法执行,可决定是否执行目标方法

+
@Around("execution(* com.example.controller..*.*(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
Object result = pjp.proceed(); // 执行目标方法
long cost = System.currentTimeMillis() - start;
log.info("耗时: " + cost + "ms");
return result;
}
+
+
+ +
+

🧪 在线测试

+

调用下面的 API,然后查看控制台日志观察 AOP 执行顺序

+ + +
+
+ +
+

📝 切入点表达式

+ + + + + + + +
表达式含义
execution(* *(..))匹配所有方法
execution(* com.example.service.*.*(..))service包下所有方法
execution(* com.example.service..*.*(..))service包及子包所有方法
execution(public * *(..))所有public方法
@annotation(org.springframework.transaction.annotation.Transactional)带@Transactional的方法
+
+
+ + + + \ No newline at end of file diff --git a/target/classes/static/api.html b/target/classes/static/api.html new file mode 100644 index 0000000..d801914 --- /dev/null +++ b/target/classes/static/api.html @@ -0,0 +1,280 @@ + + + + + + API 测试面板 - Spring Boot + + + +
+
+

🔌 API 测试面板

+

在线测试所有 RESTful API

+
+ + + +
+
👥 用户 API
+
📦 产品 API
+
🛒 订单 API
+
📚 学习 API
+
+ + +
+
+

用户管理 API

+
+
+ GET + 获取所有用户 +
/api/users
+ +
+
+ +
+ GET + 获取单个用户 +
/api/users/{id}
+ + +
+
+ +
+ POST + 创建用户 +
/api/users
+ + +
+
+ +
+ GET + 搜索用户 +
/api/users/search?username={name}
+ + +
+
+
+
+
+ + +
+
+

产品管理 API

+
+
+ GET + 获取所有产品 +
/api/products
+ +
+
+ +
+ POST + 创建产品 +
/api/products
+ + +
+
+
+
+
+ + +
+
+

订单管理 API (演示事务)

+
+
+ GET + 获取所有订单 +
/api/orders
+ +
+
+ +
+ POST + 创建订单 +
/api/orders
+ + +
+
+ +
+ POST + 创建订单(触发回滚) +
/api/orders (rollback=true)
+ + +
+
+
+
+
+ + +
+
+

学习 API

+
+
+ GET + IoC - 查看所有 Bean +
/api/learning/ioc/beans
+ +
+
+ +
+ GET + AOP - 概念 +
/api/learning/aop/concepts
+ +
+
+ +
+ GET + MyBatis - 配置 +
/api/learning/mybatis/config
+ +
+
+ +
+ GET + 事务 - 传播行为 +
/api/learning/transaction/propagation
+ +
+
+
+
+
+
+ + + + \ No newline at end of file diff --git a/target/classes/static/index.html b/target/classes/static/index.html new file mode 100644 index 0000000..d137181 --- /dev/null +++ b/target/classes/static/index.html @@ -0,0 +1,203 @@ + + + + + + Spring Boot 学习中心 + + + +
+
+

🍃 Spring Boot 学习中心

+

交互式学习 Spring 核心功能 | IoC · AOP · MyBatis · 事务

+
+ +
+
+
-
+
已加载 Bean
+
+
+
-
+
用户数量
+
+
+
-
+
产品数量
+
+
+
-
+
订单数量
+
+
+ + + +
+
+

📦 IoC 容器

+

理解 Spring 的核心:控制反转和依赖注入。学习 Bean 的生命周期、作用域和各种注入方式。

+
    +
  • Bean 生命周期演示
  • +
  • 依赖注入方式对比
  • +
  • Bean 作用域详解
  • +
  • 性能统计面板
  • +
+ 开始学习 → +
+ +
+

🔪 AOP 切面编程

+

掌握面向切面编程,实现日志、性能监控、事务等横切关注点的模块化管理。

+
    +
  • 5 种通知类型演示
  • +
  • 切入点表达式语法
  • +
  • 实时日志展示
  • +
  • 性能监控切面
  • +
+ 开始学习 → +
+ +
+

💾 MyBatis 集成

+

学习 MyBatis 与 Spring Boot 的整合,对比 JPA,掌握动态 SQL 和缓存机制。

+
    +
  • MyBatis vs JPA 对比
  • +
  • 动态 SQL 语法
  • +
  • 一级/二级缓存演示
  • +
  • 批量操作示例
  • +
+ 开始学习 → +
+ +
+

🔄 事务管理

+

深入理解 Spring 声明式事务,掌握传播行为和隔离级别的实际应用。

+
    +
  • 事务传播行为
  • +
  • 事务隔离级别
  • +
  • 回滚机制演示
  • +
  • 订单创建场景
  • +
+ 开始学习 → +
+ +
+

👥 用户管理 CRUD

+

完整的 RESTful API 示例,演示增删改查操作和参数验证。

+
    +
  • RESTful 设计规范
  • +
  • 参数验证
  • +
  • 异常处理
  • +
  • 交互式测试
  • +
+ 开始学习 → +
+ +
+

🔌 API 测试面板

+

在线测试所有 API 接口,查看请求响应,理解 RESTful API 工作原理。

+
    +
  • 用户 API
  • +
  • 产品 API
  • +
  • 订单 API
  • +
  • 学习 API
  • +
+ 开始测试 → +
+
+ +
+

🚀 快速开始 - API 示例

+
+GET  /api/users           # 获取所有用户
+GET  /api/users/{id}       # 获取单个用户
+POST /api/users           # 创建用户
+PUT  /api/users/{id}       # 更新用户
+DEL  /api/users/{id}       # 删除用户
+
+GET  /api/learning/ioc/beans      # 查看所有 Bean
+GET  /api/learning/aop/concepts   # AOP 概念
+GET  /api/learning/mybatis/cache  # 缓存机制
+GET  /api/learning/transaction/propagation  # 传播行为
+            
+
+ +
+

🍃 Spring Boot 学习脚手架 | H2 控制台 (JDBC: jdbc:h2:file:~/h2/springboot_scaffold, 用户: sa)

+
+
+ + + + \ No newline at end of file diff --git a/target/classes/static/ioc.html b/target/classes/static/ioc.html new file mode 100644 index 0000000..473f3f1 --- /dev/null +++ b/target/classes/static/ioc.html @@ -0,0 +1,175 @@ + + + + + + IoC 容器学习 - Spring Boot + + + +
+
+

📦 IoC 容器学习

+

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

+
+ + + +
+

📚 核心概念

+
+

什么是 IoC?

+

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

+

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

+
+
+

为什么用 IoC?

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

🔍 查看所有 Bean

+

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

+ + +
+
+ +
+

📊 Bean 作用域

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

⚡ 性能统计

+

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

+ + +
+
+ +
+

💉 依赖注入方式对比

+ + + + + +
方式优点缺点推荐度
构造器注入明确依赖、不可变、易测试参数多时代码长⭐⭐⭐⭐⭐
Setter 注入可选依赖、灵活可能为 null⭐⭐⭐
字段注入代码简洁隐藏依赖、难测试
+
+
+ + + + \ No newline at end of file diff --git a/target/classes/static/mybatis.html b/target/classes/static/mybatis.html new file mode 100644 index 0000000..f4b7d19 --- /dev/null +++ b/target/classes/static/mybatis.html @@ -0,0 +1,176 @@ + + + + + + MyBatis 学习 - Spring Boot + + + +
+
+

💾 MyBatis 学习

+

半自动 ORM 框架 - SQL 与 Java 对象的映射

+
+ + + +
+

📊 MyBatis vs JPA 对比

+
+
+

MyBatis

+

优点:

+
    +
  • ✅ SQL 灵活可控
  • +
  • ✅ 性能优化方便
  • +
  • ✅ 复杂查询友好
  • +
  • ✅ 易于 DBA 协作
  • +
+

缺点:

+
    +
  • ❌ SQL 与代码耦合
  • +
  • ❌ 数据库迁移成本高
  • +
  • ❌ 需要手写 SQL
  • +
+

适用:复杂查询、性能要求高

+
+
+

JPA/Hibernate

+

优点:

+
    +
  • ✅ 面向对象
  • +
  • ✅ 数据库无关
  • +
  • ✅ 开发效率高
  • +
  • ✅ 自动维护
  • +
+

缺点:

+
    +
  • ❌ 复杂查询困难
  • +
  • ❌ 性能调优复杂
  • +
  • ❌ 学习曲线陡
  • +
+

适用:标准 CRUD、快速开发

+
+
+
+ +
+

🚀 快速体验

+ + + +
+
+ +
+

💡 缓存机制

+
+

一级缓存 (SqlSession 级别)

+

默认开启,同一 SqlSession 内相同查询只执行一次 SQL

+
// 第一次查询 - 执行 SQL
User u1 = mapper.findById(1L);
// 第二次查询 - 命中缓存,不执行 SQL
User u2 = mapper.findById(1L);
+
+
+

二级缓存 (Mapper 级别)

+

需配置 @CacheNamespace,多个 SqlSession 共享

+
@Mapper
@CacheNamespace
public interface UserMapper { ... }
+
+

💡 验证方式:查看控制台 SQL 日志,缓存命中时不打印 SQL

+
+ +
+

📝 常用注解

+ + + + + + + + + + +
注解用途
@Mapper标记为 MyBatis Mapper 接口
@Select查询 SQL
@Insert插入 SQL
@Update更新 SQL
@Delete删除 SQL
@Param参数命名
@Options额外选项(如返回自增ID)
@CacheNamespace启用二级缓存
+
+ +
+

🔧 动态 SQL 示例

+
<select id="findUsers">
SELECT * FROM users
<where>
<if test="name != null">
AND name LIKE CONCAT('%', #{name}, '%')
</if>
<if test="email != null">
AND email = #{email}
</if>
</where>
</select>
+
+
+ + + + \ No newline at end of file diff --git a/target/classes/static/transaction.html b/target/classes/static/transaction.html new file mode 100644 index 0000000..9ce3f5f --- /dev/null +++ b/target/classes/static/transaction.html @@ -0,0 +1,191 @@ + + + + + + 事务管理学习 - Spring Boot + + + +
+
+

🔄 事务管理

+

声明式事务 - @Transactional

+
+ + + +
+

📚 ACID 特性

+
+
+

A

+

Atomicity

+

原子性

+

事务是不可分割的工作单位

+
+
+

C

+

Consistency

+

一致性

+

数据库状态保持一致

+
+
+

I

+

Isolation

+

隔离性

+

事务之间相互隔离

+
+
+

D

+

Durability

+

持久性

+

提交后永久保存

+
+
+
+ +
+

🚀 传播行为 (Propagation)

+
+
+

REQUIRED (默认)

+

有事务则加入,无则新建

+

最常用,适合大多数业务方法

+
+
+

REQUIRES_NEW

+

总是新建事务,挂起当前事务

+

适合日志记录、独立子任务

+
+
+

SUPPORTS

+

有事务则加入,无则以非事务运行

+

适合查询方法

+
+
+

NOT_SUPPORTED

+

以非事务运行,挂起当前事务

+

不需要事务的操作

+
+
+

MANDATORY

+

必须在事务中运行,否则抛异常

+

强制要求事务

+
+
+

NEVER

+

不能在事务中运行,否则抛异常

+

确保无事务

+
+
+ +
+
+ +
+

🔒 隔离级别 (Isolation)

+ + + + + + +
级别脏读不可重复读幻读说明
READ_UNCOMMITTED读未提交,性能最高
READ_COMMITTED读已提交,Oracle默认
REPEATABLE_READ可重复读,MySQL默认
SERIALIZABLE串行化,性能最低
+

✅ = 防止该问题 | ❌ = 可能出现该问题

+ +
+
+ +
+

🧪 事务回滚演示

+

创建订单时触发异常,观察事务回滚效果

+ +
+
+
+ + + + \ No newline at end of file diff --git a/target/classes/static/users.html b/target/classes/static/users.html new file mode 100644 index 0000000..bdde258 --- /dev/null +++ b/target/classes/static/users.html @@ -0,0 +1,278 @@ + + + + + + 用户管理 - Spring Boot CRUD + + + +
+
+

👥 用户管理

+

RESTful CRUD 操作演示

+
+ + + +
+

➕ 创建/编辑用户

+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+ + +
+
+
+ +
+

📋 用户列表

+ + + + + + + + + + + + + + + + +
ID用户名邮箱手机号状态创建时间操作
加载中...
+
+
+ + + + \ No newline at end of file diff --git a/target/maven-archiver/pom.properties b/target/maven-archiver/pom.properties new file mode 100644 index 0000000..37882b4 --- /dev/null +++ b/target/maven-archiver/pom.properties @@ -0,0 +1,3 @@ +artifactId=springboot-scaffold +groupId=com.example +version=1.0.0 diff --git a/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst new file mode 100644 index 0000000..feed0ef --- /dev/null +++ b/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst @@ -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 diff --git a/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst new file mode 100644 index 0000000..9a56105 --- /dev/null +++ b/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst @@ -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 diff --git a/target/springboot-scaffold-1.0.0.jar b/target/springboot-scaffold-1.0.0.jar new file mode 100644 index 0000000..6445a16 Binary files /dev/null and b/target/springboot-scaffold-1.0.0.jar differ diff --git a/target/springboot-scaffold-1.0.0.jar.original b/target/springboot-scaffold-1.0.0.jar.original new file mode 100644 index 0000000..e5d4006 Binary files /dev/null and b/target/springboot-scaffold-1.0.0.jar.original differ