ai全栈day06
AI 全栈工程师学习日记:Spring Boot 自动配置、AOP 切面、MVC 流程与数据库设计实战
今天简直是 Spring 框架的"深度一日游"!从Spring Boot 自动配置的底层原理,到AOP 面向切面编程的优雅解耦;从Spring MVC 请求处理的完整链路,到数据库 ER 图与三范式的设计实践。如果说昨天是站在 Spring 门口参观,今天就是拿着放大镜研究它的五脏六腑。AutoConfiguration 的魔法不再神秘,AOP 的代理机制也终于理解了为什么this调用会失效。最爽的是亲手设计了一套电商数据库,从 ER 图到建表语句,体会到了架构设计的美感。
Spring BootAOPSpring MVC数据库设计自动配置
8h
今日学习
06/70
总进度
8
学习阶段
目录
- 一、Spring Boot 自动配置原理:魔法背后的条件注解
- 二、Spring AOP 面向切面编程:横切关注点的优雅解耦
- 三、Spring MVC 请求处理流程:从浏览器到 Controller 的完整旅程
- 四、数据库设计实战:ER 图、三范式与索引设计
- 五、今日踩坑记录
- 六、收获总结与学习感悟
- 七、明日学习计划
一、Spring Boot 自动配置原理:魔法背后的条件注解
初学 Spring Boot 时最震撼的体验就是:加一个依赖、写几行配置,整个功能就自动跑起来了。这种"约定大于配置"的魔法是怎么实现的?今天终于揭开了@SpringBootApplication的神秘面纱。
1.1 @SpringBootApplication 的三层拆解
@SpringBootApplication其实是一个"组合注解",它等同于以下三个注解的叠加:
Java@SpringBootApplication 源码本质
@SpringBootConfiguration // 本质就是 @Configuration,标记这是配置类 @EnableAutoConfiguration // 开启自动配置的核心 @ComponentScan( // 组件扫描,默认扫描当前包及子包 excludeFilters = {@Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class)} ) public @interface SpringBootApplication {} // 等价写法 @Configuration @EnableAutoConfiguration @ComponentScan public class MyApplication { ... }1.2 @EnableAutoConfiguration 的工作流程
自动配置的入口是@EnableAutoConfiguration,它通过@Import导入了一个关键类AutoConfigurationImportSelector,整个加载流程如下:
- 读取候选配置清单:从所有依赖 jar 包的
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件中读取全限定类名列表 - 条件过滤:对每个候选配置类,检查其上的条件注解(如
@ConditionalOnClass),判断是否满足激活条件 - 去重与排序:通过
@AutoConfigureBefore/@AutoConfigureAfter控制配置顺序 - 注册 Bean:将最终筛选出的配置类注册到 Spring 容器
Spring Boot 2.7+ 的配置文件变化
Spring Boot 2.7 之前,自动配置类清单写在META-INF/spring.factories中,键为org.springframework.boot.autoconfigure.EnableAutoConfiguration。2.7 之后引入了新的META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件,每行一个全限定类名,更加直观。Spring Boot 3.x 已经完全迁移到新格式。
1.3 条件注解:自动配置的"智能开关"
条件注解是自动配置的"灵魂",它们决定了在什么情况下才加载某个 Bean:
| 条件注解 | 作用 | 示例场景 |
|---|---|---|
@ConditionalOnClass | 类路径存在指定类时才生效 | 检测到DataSource.class才配置数据源 |
@ConditionalOnMissingClass | 类路径不存在指定类时才生效 | 用户未引入 ShardingJDBC 时才配置默认数据源 |
@ConditionalOnBean | 容器中存在指定 Bean 时才生效 | 存在RedisConnectionFactory才配置 RedisTemplate |
@ConditionalOnMissingBean | 容器中不存在指定 Bean 时才生效 | 用户未自定义ObjectMapper时才创建默认的 |
@ConditionalOnProperty | 指定配置属性满足条件时才生效 | spring.datasource.url存在时才初始化连接池 |
@ConditionalOnWebApplication | 是 Web 应用时才生效 | 只在 Web 环境下配置 DispatcherServlet |
1.4 手写一个自定义 Starter
为了彻底理解 Starter 机制,我动手写了一个极简的日志拦截 Starter——引入依赖后自动为所有 Controller 方法添加执行耗时打印。
JavaLogInterceptorAutoConfiguration.java
package com.example.autoconfig; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration(proxyBeanMethods = false) @ConditionalOnWebApplication @ConditionalOnProperty(prefix = "log.interceptor", name = "enabled", havingValue = "true", matchIfMissing = true) public class LogInterceptorAutoConfiguration { @Bean public LogInterceptor logInterceptor() { return new LogInterceptor(); } @Bean public LogInterceptorConfigurer logInterceptorConfigurer(LogInterceptor interceptor) { return new LogInterceptorConfigurer(interceptor); } }JavaLogInterceptor.java
import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; public class LogInterceptor implements HandlerInterceptor { private static final String START_TIME = "startTime"; @Override public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler) { req.setAttribute(START_TIME, System.currentTimeMillis()); return true; } @Override public void afterCompletion(HttpServletRequest req, HttpServletResponse res, Object handler, Exception ex) { long start = (Long) req.getAttribute(START_TIME); long cost = System.currentTimeMillis() - start; System.out.printf("[%s %s] 耗时: %dms%n", req.getMethod(), req.getRequestURI(), cost); } }PlainMETA-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
com.example.autoconfig.LogInterceptorAutoConfigurationStarter 命名规范
官方 Starter 命名格式为spring-boot-starter-xxx,如spring-boot-starter-web。自定义 Starter 推荐命名为xxx-spring-boot-starter,如my-log-spring-boot-starter,以区分官方与第三方。
***
二、Spring AOP 面向切面编程:横切关注点的优雅解耦
AOP(Aspect-Oriented Programming)是 Spring 框架的两大基石之一。它的核心思想是:把散布在业务代码各处的横切关注点(日志、权限、事务、监控)提取出来,集中管理,让业务代码只关心业务本身。
2.1 AOP 核心概念速览
- Aspect(切面):横切关注点的模块化,一个切面包含多个通知和切点
- Join Point(连接点):程序执行过程中的某个点,如方法调用、异常抛出
- Pointcut(切点):匹配连接点的表达式,决定通知在何处生效
- Advice(通知):切面在特定连接点采取的行动,如前置通知、后置通知
- Target(目标对象):被代理的原始对象
- Proxy(代理):AOP 框架创建的对象,包含目标对象和通知逻辑
2.2 切点表达式:精准定位目标方法
切点表达式是 AOP 的"导航仪",最常用的execution表达式语法如下:
Java切点表达式示例
// 匹配 UserService 接口的所有方法 execution(* com.example.service.UserService.*(..)) // 匹配 service 包下所有类的所有方法 execution(* com.example.service.*.*(..)) // 匹配所有以 save 开头的方法,任意参数,返回任意类型 execution(* save*(..)) // 匹配所有 public 方法 execution(public * *(..)) // 匹配第一个参数为 String,后面任意参数的方法 execution(* *(String, ..)) // 组合切点:service 包 && 方法名以 find 开头 @Pointcut("execution(* com.example.service.*.*(..))") public void serviceLayer() {} @Pointcut("execution(* find*(..))") public void findMethods() {} @Pointcut("serviceLayer() && findMethods()") public void serviceFindOps() {}2.3 五种通知类型与执行顺序
| 通知类型 | 注解 | 执行时机 |
|---|---|---|
| 前置通知 | @Before | 目标方法执行之前 |
| 后置通知 | @After | 目标方法执行之后(无论是否异常) |
| 返回通知 | @AfterReturning | 目标方法正常返回后 |
| 异常通知 | @AfterThrowing | 目标方法抛出异常后 |
| 环绕通知 | @Around | 包裹目标方法,可控制是否执行 |
JavaLoggingAspect.java — 统一日志切面
import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.*; import org.springframework.stereotype.Component; @Aspect @Component public class LoggingAspect { // 定义切点:所有 Controller 层方法 @Pointcut("execution(* com.example.controller.*.*(..))") public void controllerLayer() {} @Before("controllerLayer()") public void logBefore(JoinPoint jp) { String method = jp.getSignature().toShortString(); Object[] args = jp.getArgs(); System.out.println("[前置] 调用: " + method + ", 参数: " + Arrays.toString(args)); } @AfterReturning(pointcut = "controllerLayer()", returning = "result") public void logAfterReturning(JoinPoint jp, Object result) { System.out.println("[返回] " + jp.getSignature().toShortString() + " 结果: " + result); } @AfterThrowing(pointcut = "controllerLayer()", throwing = "ex") public void logException(JoinPoint jp, Exception ex) { System.err.println("[异常] " + jp.getSignature().toShortString() + " 异常: " + ex.getMessage()); } // 环绕通知:最强大,通常用于性能监控 @Around("controllerLayer()") public Object logAround(ProceedingJoinPoint pjp) throws Throwable { long start = System.currentTimeMillis(); String method = pjp.getSignature().toShortString(); try { Object result = pjp.proceed(); // 执行目标方法 long cost = System.currentTimeMillis() - start; System.out.println("[环绕] " + method + " 耗时: " + cost + "ms"); return result; } catch (Throwable t) { long cost = System.currentTimeMillis() - start; System.err.println("[环绕] " + method + " 异常中断, 已耗时: " + cost + "ms"); throw t; } } }2.4 代理机制:JDK 动态代理 vs CGLIB
AOP 的实现依赖代理模式,Spring 提供了两种代理方式:
| 维度 | JDK 动态代理 | CGLIB |
|---|---|---|
| 实现原理 | 实现目标类的接口,生成接口代理 | 继承目标类,生成子类代理 |
| 适用对象 | 实现了接口的类 | 没有实现接口的类 |
| 目标类要求 | 必须有接口 | 不能是 final 类,方法不能是 final |
| 性能 | 稍慢(反射调用) | 更快(方法索引直接调用) |
| Spring 默认策略 | 目标有接口时优先使用 | 无接口时自动 fallback |
this 调用不走代理 — AOP 的经典陷阱
同一类中方法 A 调用方法 B(B 有 AOP 通知),B 的通知不会生效。原因:外部调用通过代理对象,包含通知逻辑;而内部this.B()调用的是目标对象本身,绕过了代理。解决:将 B 抽到另一个 Bean 中;或注入自身代理;或改用 AspectJ 编译期织入。
***
三、Spring MVC 请求处理流程:从浏览器到 Controller 的完整旅程
Spring MVC 是 Spring 框架的 Web 模块,也是目前 Java Web 开发的事实标准。理解它的请求处理流程,对排查接口问题、优化性能、扩展框架都至关重要。
3.1 DispatcherServlet:前端控制器的中央调度
Spring MVC 的核心是DispatcherServlet,它遵循"前端控制器"设计模式,所有请求都先经过它,再由它分发给后续的处理器。一个 HTTP 请求的完整处理链路如下:
- 请求到达:HTTP 请求进入
DispatcherServlet.doDispatch() - 查找处理器:通过
HandlerMapping找到处理该请求的HandlerExecutionChain(包含 Controller 方法和拦截器链) - 查找适配器:通过
HandlerAdapter找到能执行该处理器的适配器(如RequestMappingHandlerAdapter) - 参数解析:适配器调用
HandlerMethodArgumentResolver解析请求参数(@RequestParam、@RequestBody等) - 执行处理器:反射调用 Controller 方法,执行业务逻辑
- 处理返回值:通过
HandlerMethodReturnValueHandler处理返回值,如@ResponseBody会用HttpMessageConverter序列化为 JSON - 视图渲染/响应:如果是页面请求则通过
ViewResolver解析视图;如果是 REST API 则直接返回 JSON - 异常处理:过程中任何异常都会被
HandlerExceptionResolver捕获,最终进入@ControllerAdvice处理
3.2 统一返回格式与全局异常处理
生产环境中,REST API 必须统一返回格式,方便前端处理和错误追踪。
JavaApiResponse.java — 统一返回体
public class ApiResponse<T> { private int code; // 业务状态码 0=成功 private String message; // 提示信息 private T data; // 数据 private long timestamp; // 时间戳 public static <T> ApiResponse<T> success(T data) { ApiResponse<T> r = new ApiResponse<>(); r.code = 0; r.message = "success"; r.data = data; r.timestamp = System.currentTimeMillis(); return r; } public static <T> ApiResponse<T> error(int code, String message) { ApiResponse<T> r = new ApiResponse<>(); r.code = code; r.message = message; r.timestamp = System.currentTimeMillis(); return r; } // getters & setters... }JavaGlobalExceptionHandler.java — 全局异常处理器
import org.springframework.web.bind.annotation.*; @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(BusinessException.class) public ApiResponse<Void> handleBusiness(BusinessException ex) { return ApiResponse.error(ex.getCode(), ex.getMessage()); } @ExceptionHandler(MethodArgumentNotValidException.class) public ApiResponse<Void> handleValidation(MethodArgumentNotValidException ex) { String msg = ex.getBindingResult().getFieldErrors().stream() .map(e -> e.getField() + ": " + e.getDefaultMessage()) .collect(Collectors.joining(", ")); return ApiResponse.error(400, msg); } @ExceptionHandler(Exception.class) public ApiResponse<Void> handleAll(Exception ex) { // 生产环境不要返回详细异常信息给客户端 return ApiResponse.error(500, "服务器内部错误"); } }@ControllerAdvice vs @RestControllerAdvice
@RestControllerAdvice等于@ControllerAdvice + @ResponseBody,意味着所有异常处理方法都会将返回值序列化为 JSON/XML。在纯 REST API 项目中统一使用@RestControllerAdvice,避免每个方法都加@ResponseBody。
***
四、数据库设计实战:ER 图、三范式与索引设计
好的数据库设计是系统稳定的基石。今天从零开始设计了一套简化版电商数据库,经历了需求分析 → ER 图 → 范式规范化 → 建表 → 索引设计的完整流程。
4.1 需求分析与实体识别
简化版电商系统需要支持:用户注册登录、商品浏览、购物车、下单、支付、订单状态跟踪。
- 用户(User):用户ID、手机号、密码、昵称、创建时间
- 商品(Product):商品ID、名称、描述、价格、库存、分类ID
- 分类(Category):分类ID、名称、父分类ID
- 订单(Order):订单ID、用户ID、总金额、状态、创建时间
- 订单项(OrderItem):订单项ID、订单ID、商品ID、数量、单价
- 购物车(CartItem):购物车ID、用户ID、商品ID、数量
4.2 ER 图与关系梳理
实体之间的关系:
- 用户 — 订单:1:N(一个用户有多个订单)
- 订单 — 订单项:1:N(一个订单包含多个订单项)
- 商品 — 订单项:1:N(一个商品出现在多个订单项中)
- 分类 — 商品:1:N(一个分类下有多件商品)
- 用户 — 购物车:1:N(一个用户有多条购物车记录)
- 商品 — 购物车:1:N(一个商品被多个用户加入购物车)
4.3 三范式:规范化的艺术
三范式是关系型数据库设计的黄金法则,目的是消除数据冗余和更新异常。
| 范式 | 核心要求 | 解决什么问题 |
|---|---|---|
| 1NF | 每个字段都是原子的,不可再分 | 消除重复组和可再分字段 |
| 2NF | 满足 1NF,且非主属性完全依赖主键 | 消除部分依赖(主要针对联合主键) |
| 3NF | 满足 2NF,且消除传递依赖 | 消除间接依赖,避免数据冗余 |
反范式设计:适当冗余换取性能
三范式不是圣经。在高并发读场景中,适当的反范式设计(冗余字段)可以大幅减少 JOIN 查询,提升性能。例如:订单表中冗余total_amount(可通过订单项计算得出),以及user_phone(可从用户表查询),目的是避免订单查询时关联多张表。冗余的代价是数据一致性维护更复杂,需要根据业务场景权衡。
4.4 建表语句与索引设计
SQLecommerce_schema.sql
-- 用户表 CREATE TABLE `user` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '用户ID', `phone` VARCHAR(20) NOT NULL COMMENT '手机号', `password` VARCHAR(128) NOT NULL COMMENT '加密密码', `nickname` VARCHAR(50) DEFAULT '' COMMENT '昵称', `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_phone` (`phone`), -- 手机号唯一 KEY `idx_created_at` (`created_at`) -- 按时间范围查询 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; -- 商品表 CREATE TABLE `product` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, `name` VARCHAR(200) NOT NULL COMMENT '商品名称', `description` TEXT COMMENT '商品描述', `price` DECIMAL(10,2) NOT NULL COMMENT '售价', `stock` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '库存', `category_id` BIGINT UNSIGNED NOT NULL COMMENT '分类ID', `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_category` (`category_id`), -- 按分类查询 KEY `idx_price` (`price`), -- 按价格排序/筛选 FULLTEXT KEY `ft_name` (`name`) -- 商品名称全文搜索 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品表'; -- 订单表 CREATE TABLE `order` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, `user_id` BIGINT UNSIGNED NOT NULL COMMENT '用户ID', `total_amount` DECIMAL(12,2) NOT NULL COMMENT '订单总金额', `status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态:0待支付 1已支付 2已发货 3已完成 4已取消', `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, `paid_at` TIMESTAMP NULL DEFAULT NULL COMMENT '支付时间', PRIMARY KEY (`id`), KEY `idx_user_id` (`user_id`), -- 查询用户订单列表 KEY `idx_status_created` (`status`,`created_at`) -- 按状态+时间查询 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表'; -- 订单项表 CREATE TABLE `order_item` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, `order_id` BIGINT UNSIGNED NOT NULL COMMENT '订单ID', `product_id` BIGINT UNSIGNED NOT NULL COMMENT '商品ID', `quantity` INT UNSIGNED NOT NULL DEFAULT 1 COMMENT '数量', `unit_price` DECIMAL(10,2) NOT NULL COMMENT '下单时单价', PRIMARY KEY (`id`), KEY `idx_order_id` (`order_id`), -- 查询订单详情 KEY `idx_product_id` (`product_id`) -- 统计商品销量 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单项表';4.5 索引设计原则总结
- WHERE 条件字段:频繁用于过滤的字段必须建索引(如
user_id、status) - ORDER BY / GROUP BY 字段:排序和分组字段建索引可避免 filesort
- 联合索引最左前缀:
(status, created_at)可以匹配WHERE status=?或WHERE status=? AND created_at>?,但不能匹配WHERE created_at>? - 避免索引泛滥:每个额外索引都会增加写操作开销,单表索引数建议不超过 5 个
- 区分度低的字段不加单列索引:如性别(只有男女),加了也几乎没筛选效果
***
五、今日踩坑记录
坑 1:Spring Boot 3 中 WebMvcConfigurer 配置拦截器不生效
按照教程实现了WebMvcConfigurer.addInterceptors()注册拦截器,但请求时拦截器没有触发。原因:项目中同时引入了spring-boot-starter-webflux,Spring Boot 优先启用了 WebFlux 自动配置,而WebMvcConfigurer是 Servlet MVC 体系的接口,在 WebFlux 环境下不生效。解决方案:检查 pom.xml,确保没有同时引入 web 和 webflux;如果确实需要 WebFlux,拦截器应改用WebFilter实现。
坑 2:AOP 切点表达式写错导致所有方法都被拦截
本想只拦截 Controller 层方法,写了execution(* com.example.*.*(..)),结果 service、dao 层的方法也被拦截了,日志爆多。原因:com.example.*只匹配 example 下的直接子包(一级),但com.example.controller.*才是 controller 包下的类;更危险的是..和*的区别——..匹配任意层级子包,*只匹配一层。解决方案:明确指定execution(* com.example.controller..*.*(..)),用..确保只拦截 controller 包及其子包。
Java
// 错误:匹配 com.example 下所有一级子包的所有类 execution(* com.example.*.*(..)) // 正确:匹配 controller 包及其任意子包下的所有方法 execution(* com.example.controller..*.*(..))坑 3:@Transactional 放在 private 方法上不生效
给 Service 的私有方法加了@Transactional,但事务没有回滚。原因:Spring AOP 的代理机制(无论是 JDK 还是 CGLIB)都只能拦截 public 方法。private 方法无法被代理对象重写/拦截,所以事务注解被完全忽略。解决方案:将方法改为 public;或者把事务逻辑抽到另一个 Service Bean 中调用。
坑 4:MySQL DECIMAL 精度丢失问题
订单金额字段用DECIMAL(10,2)存储,但在 Java 实体中用了double类型接收,发现某些金额(如 19.99)在计算后出现19.989999999999998。原因:double/float 是二进制浮点数,无法精确表示某些十进制小数。解决方案:Java 中金额计算必须用BigDecimal,且数据库实体字段类型要与数据库一致,全程避免使用 double/float 处理金额。
Java
// 错误:double 精度丢失 double a = 19.99, b = 0.01; System.out.println(a + b); // 可能输出 20.000000000000004 // 正确:BigDecimal 精确计算 BigDecimal a = new BigDecimal("19.99"); // 必须用字符串构造 BigDecimal b = new BigDecimal("0.01"); System.out.println(a.add(b)); // 精确输出 20.00坑 5:联合索引顺序写反导致索引失效
订单表建了联合索引(created_at, status),查询WHERE status=1 AND created_at>'2026-07-01'时发现走了全表扫描。原因:联合索引遵循"最左前缀原则",索引是(created_at, status)意味着先按 created_at 排序,再按 status 排序。查询条件以status开头,跳过了最左列created_at,导致索引无法使用。解决方案:调整索引顺序为(status, created_at),或改写查询确保最左列被使用。
***
六、收获总结与学习感悟
今日收获
- Spring Boot 自动配置:理解了
@SpringBootApplication的组合本质、条件注解的过滤机制、Starter 的加载流程,并手写了一个自定义 Starter。 - Spring AOP:掌握了五种通知类型、切点表达式语法、JDK/CGLIB 代理的区别,以及
this调用不走代理的经典陷阱。 - Spring MVC:理清了 DispatcherServlet → HandlerMapping → HandlerAdapter → Controller → 异常处理的完整链路,实现了统一返回格式和全局异常处理。
- 数据库设计:从需求分析到 ER 图,从三范式到反范式设计,从建表到索引优化,完整走了一遍数据库架构设计流程。
- 工程意识:体会到了"约定大于配置"和"解耦"在实际项目中的巨大价值。
学习感悟
今天学 Spring Boot 自动配置的时候,我突然有种"原来如此"的通透感。以前觉得 Spring Boot 很神奇,加个依赖就能跑,现在明白了它不过是在启动时读了一堆配置类,用条件注解判断要不要加载。魔法消失了,但美感还在——那种把复杂逻辑藏在简单接口背后的设计美感。
AOP 给我的最大启发是"关注点分离"。以前写代码时日志、权限、事务和业务逻辑混在一起,读起来像一锅粥。AOP 就像一把锋利的手术刀,把横切关注点干净利落地切出去,业务代码瞬间变得清爽。当然代价是要理解代理机制,不然就会被this调用、事务不生效这类问题搞得头大。
数据库设计是今天最有"架构师感觉"的部分。画 ER 图的时候,我在纸上涂涂改改,思考实体之间的关系是 1:N 还是 M:N,哪些字段需要冗余,索引怎么建——这些决策会直接影响系统未来几年的性能和扩展性。三范式告诉我什么是"正确"的设计,反范式教会我"正确"不一定"合适"。
明天要进入数据库性能优化和 MyBatis 的学习了,第二阶段还剩最后一天,加油!
***
七、明日学习计划
Day 07 · 2026-07-06(周一)
- 数据库性能优化:SQL 优化、慢查询分析、EXPLAIN 执行计划解读、索引优化进阶
- MyBatis / MyBatis-Plus:ORM 映射、动态 SQL、分页插件、代码生成器
- 连接池与事务:HikariCP 原理、Spring 声明式事务传播行为
- 第二阶段项目启动:多模态智能写手 — 需求分析与架构设计