Loading

【WebWiki】04 · Spring Boot

02 Spring Boot

WebWiki · 后端框架篇

面向已掌握 Java/Spring 基础、需深入理解 Spring Boot 机制的工程师。重点在 IoC、AOP、自动配置、事务传播等"知其所以然"的内容,代码示例带中文注释。

1. 核心概念

IoC(控制反转):对象的创建与装配交给容器管理。DI(依赖注入) 是实现手段。传统方式里 new 出的对象自己管理依赖,IoC 把"控制权"反转给 Spring 容器,容器按配置把依赖"注入"进来,降低耦合、便于测试与替换实现。

@Service                      // 声明为 Bean,默认单例
@RequiredArgsConstructor      // Lombok 构造器注入(推荐,利于单测)
public class OrderService {
    private final OrderRepository orderRepository;   // 容器在启动时注入
    private final PaymentClient paymentClient;
}
注入方式 建议
构造器注入 推荐:不可变(final)、可暴露循环依赖问题、方便单测
Setter 注入 可选:适合可选依赖、需要重新赋值
字段注入 @Autowired 尽量避免:无法声明 final、单测难 mock、隐藏依赖

1.1 Bean 的作用域

scope 说明
singleton(默认) 容器级单例,整个应用共享一个实例
prototype 每次获取 getBean 或注入都新建
request / session Web 环境,每次请求/会话一个
application ServletContext 级单例

单例的线程安全:单例 Bean 被所有请求共享,若内部有可变成员变量就会有并发问题。无状态 Service(只调方法不改字段)是安全的;需要保存请求态请用方法局部变量或 ThreadLocal。

1.2 Bean 生命周期(面试高频)

容器启动到 Bean 可用经历:

  1. 实例化:反射调用构造器 new 出对象(此时依赖还是 null);
  2. 属性填充(populate):注入 @Autowired/@Value 等依赖;
  3. Aware 回调:BeanNameAware / BeanFactoryAware / ApplicationContextAware 注入容器信息;
  4. 初始化前:@PostConstruct 注解方法 / BeanPostProcessor.postProcessBeforeInitialization;
  5. 初始化:InitializingBean.afterPropertiesSet;
  6. 初始化后:BeanPostProcessor.postProcessAfterInitialization(AOP 代理就在这里生成);
  7. 就绪:Bean 可被使用;
  8. 销毁:容器关闭时 @PreDestroy / DisposableBean.destroy。
@Component
public class LifecycleDemo implements InitializingBean, DisposableBean {
    @PostConstruct
    void init() { System.out.println("1. @PostConstruct"); }   // 早于 afterPropertiesSet
    @Override
    public void afterPropertiesSet() { System.out.println("2. 属性填完"); }
    @PreDestroy
    void cleanup() { System.out.println("3. 容器关闭清理"); }
}

BeanPostProcessor 是 Spring 扩展的核心钩子:AOP、事务(@Transactional)、@Async 全部靠它在初始化后织入代理。理解这一点就懂了"为什么 @Transactional 必须外部调用才生效"——代理只在 Bean 完全初始化后才套上去。

2. 高频注解速查

注解 作用
@RestController @Controller + @ResponseBody,返回 JSON
@RequestMapping / @GetMapping / @PostMapping 路由映射
@PathVariable / @RequestParam / @RequestBody 取参:路径 / query / JSON 体
@Service @Repository @Component 分层声明 Bean
@Configuration @Bean Java 配置类(方法返回的对象注册为 Bean)
@Value / @ConfigurationProperties 读配置(前者单值,后者批量绑定前缀)
@Transactional 声明式事务
@Async @Scheduled 异步方法 / 定时任务
@ControllerAdvice @ExceptionHandler 全局异常处理

2.1 容易被忽略的注解细节

  • @Configuration 的 proxyBeanMethods:true(默认)时 @Bean 方法互相调用返回同一单例(通过 CGLIB 代理);设 false(Lite 模式)则每次调用都是新实例,启动更快,适合纯注册 Bean 的配置类。
  • @Import:导入其他配置类 / ImportSelector / ImportBeanDefinitionRegistrar,是自动配置的底层手段之一。
  • @Conditional 系列:@ConditionalOnClass / @ConditionalOnMissingBean / @ConditionalOnProperty —— 自动配置按条件装配的开关。
  • @Lazy:延迟初始化(首次使用时才创建),可打破部分循环依赖、加速启动。
  • @Order / @Priority:控制同类型 Bean 的注入顺序或 @Bean 候选优先级。

3. 自动配置原理(面试与排错必备)

3.1 全流程

  1. @SpringBootApplication 包含 @EnableAutoConfiguration;
  2. 启动时读取所有依赖 jar 中 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports(2.7 前为 spring.factories);
  3. 每个自动配置类用 @ConditionalOnClass / @ConditionalOnMissingBean 条件装配;
  4. 覆盖默认行为的正道:自己定义同类型 Bean(@ConditionalOnMissingBean 会退让),或改配置项。

3.2 以 DataSource 自动配置为例

spring-boot-autoconfigure.jar
└── META-INF/spring/
    └── org.springframework.boot.autoconfigure.AutoConfiguration.imports
        → 含 org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration

DataSourceAutoConfiguration 大致逻辑:

@AutoConfiguration
@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })  // 类路径有这些类才生效
@EnableConfigurationProperties(DataSourceProperties.class)
public class DataSourceAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean   // 关键:用户自己定义了 DataSource 就跳过自动配置
    @ConditionalOnProperty(prefix = "spring.datasource", name = "url")
    public DataSource dataSource(DataSourceProperties props) {
        return props.initializeDataSourceBuilder().build();   // 按 yaml 里的 url/用户名/密码建连接池
    }
}

3.3 SPI 机制

Spring Boot 的自动配置本质是 Java SPI 思想的增强:JDK 原生 SPI 读 META-INF/services,按接口全限定名找实现类;Spring 用 SpringFactoriesLoader(现 AutoConfiguration.imports)批量加载,并通过 @Conditional 做按需过滤,比原生 SPI 更灵活、不会无脑加载全部实现。

排错口诀:自动配置"不生效"十有八九是:① 依赖没引入(@ConditionalOnClass 不满足);② 你自定义的 Bean 类型/名字冲突;③ 配置项前缀写错。用 spring-boot-actuator 的 /actuator/conditions 看"哪些自动配置生效/未生效及原因",比猜快得多。

4. 配置文件

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/mall?useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password: ${DB_PWD}              # 环境变量占位,密码不进代码库
  redis:
    host: 127.0.0.0
    port: 6379

mybatis-plus:
  mapper-locations: classpath*:mapper/**/*.xml
  configuration:
    map-underscore-to-camel-case: true

# 多环境:application-dev.yml / application-prod.yml
spring:
  profiles:
    active: dev

类型安全的配置绑定:

@Data
@ConfigurationProperties(prefix = "biz.order")   // 绑定前缀,IDE 可提示
public class OrderProperties {
    private int expireMinutes = 15;   // 未支付订单超时(默认值兜底)
    private int maxItemsPerOrder = 99;
}
@Configuration
@EnableConfigurationProperties(OrderProperties.class)   // 让容器管理该配置类
public class BizConfig {
    @Bean
    public OrderExpireChecker checker(OrderProperties props) {
        return new OrderExpireChecker(props.getExpireMinutes());
    }
}

配置优先级(由高到低摘录):命令行参数 --xxx > 环境变量 > application-{profile}.yml > application.yml > 默认配置。排错时不确定哪个值生效,开 debug: true 看 configprops 报告。

5. 统一返回与全局异常

// 统一响应体
public record R<T>(int code, String msg, T data) {
    public static <T> R<T> ok(T data) { return new R<>(0, "success", data); }
    public static R<Void> fail(int code, String msg) { return new R<>(code, msg, null); }
}

@RestControllerAdvice        // = @ControllerAdvice + @ResponseBody,拦截所有 @RestController
public class GlobalExceptionHandler {
    @ExceptionHandler(BizException.class)
    public R<Void> biz(BizException e) { return R.fail(e.getCode(), e.getMessage()); }

    @ExceptionHandler(MethodArgumentNotValidException.class)   // @Validated 校验失败
    public R<Void> valid(MethodArgumentNotValidException e) {
        String msg = e.getBindingResult().getFieldErrors().stream()
                .map(f -> f.getField() + ": " + f.getDefaultMessage())
                .collect(Collectors.joining("; "));
        return R.fail(400, msg);
    }

    @ExceptionHandler(Exception.class)   // 兜底,避免把堆栈透给用户
    public R<Void> unknown(Exception e) {
        log.error("系统异常", e);
        return R.fail(500, "系统繁忙,请稍后重试");
    }
}

参数校验:

public record CreateOrderReq(
        @NotNull Long skuId,
        @Min(1) @Max(99) Integer count,                 // 数量 1~99
        @NotBlank @Pattern(regexp = "^1\\d{10}$") String phone) {}  // 手机号 11 位

@PostMapping("/orders")
public R<OrderVO> create(@RequestBody @Validated CreateOrderReq req) { ... }  // @Validated 触发校验

顺序注意:@ExceptionHandler 匹配最具体异常优先。把 Exception 兜底放最后,否则会"吞掉"更精确的 BizException。

6. 事务(@Transactional)深入

6.1 使用要点

  • 加在公共方法上,从外部调用才生效(AOP 代理限制);
  • 默认只回滚 RuntimeException,受检异常需 @Transactional(rollbackFor = Exception.class);
  • 事务内不要做 RPC/HTTP 调用与大循环(拉长连接占用);
  • self 方法内部调用事务方法无效——拆到另一个 Bean 或用 TransactionTemplate。
@Transactional(rollbackFor = Exception.class)
public void createOrder(CreateOrderReq req) {
    orderRepository.insert(order);
    stockService.deduct(req.skuId(), req.count());   // 同库本地事务一起回滚
    // 注意:发 MQ/调支付等外部动作应放在事务提交后(TransactionSynchronization 或编码式拆分)
}

6.2 七种传播行为(Propagation)

传播行为 行为 场景
REQUIRED(默认) 有事务则加入,没有则新建 绝大多数业务方法
SUPPORTS 有则加入,无则以非事务执行 查询方法可选事务
MANDATORY 必须在已有事务中,否则抛异常 强制调用方开启事务
REQUIRES_NEW 挂起当前事务,新建独立事务 日志/审计,主事务回滚不影响它
NOT_SUPPORTED 挂起当前事务,非事务执行 不需事务的批量处理
NEVER 必须在非事务中,有事务则抛异常 禁止事务上下文
NESTED 在当前事务内开保存点嵌套子事务 子操作可部分回滚(依赖 JDBC Savepoint,并非所有数据库支持)
// 示例:主事务回滚,审计日志仍要保留 —— 用 REQUIRES_NEW 隔离
@Transactional
public void place(OrderReq req) {
    orderRepo.save(order);                 // 主事务
    auditService.log(req);                 // 该方法 @Transactional(REQUIRES_NEW),独立提交
    if (req.getCount() <= 0) throw new BizException("数量非法"); // 主事务回滚,但日志已落库
}

REQUIRES_NEW vs NESTED:REQUIRES_NEW 是真正两个物理事务,外层回滚不影响内层;NESTED 是同一事务内的保存点,外层回滚会把内层一起回滚(内层可单独回滚到保存点)。

6.3 隔离级别与只读优化

@Transactional(isolation = Isolation.READ_COMMITTED, readOnly = true)  // 只读提示让数据库/ORM 优化
public List<Order> query(Long userId) { ... }
隔离级别 脏读 不可重复读 幻读
READ_UNCOMMITTED 可能 可能 可能
READ_COMMITTED(常用) 防止 可能 可能
REPEATABLE_READ(MySQL 默认) 防止 防止 基本防止(MVCC + 间隙锁)
SERIALIZABLE 防止 防止 防止(串行,性能最差)

7. AOP 切面实战与原理

7.1 动态代理两种方式

Spring AOP 默认按目标对象类型选代理:

  • JDK 动态代理:目标实现接口时用,基于 InvocationHandler,生成的代理类也实现这些接口;
  • CGLIB:目标无接口 / 类代理时用,继承目标类重写方法(因此 final 类/方法无法被代理)。
@Aspect @Component
public class LogAspect {
    // 切点:匹配 com.mall 下所有 service 的所有方法
    @Pointcut("execution(* com.mall..service..*(..))")
    public void biz() {}

    @Around("biz()")
    public Object log(ProceedingJoinPoint pjp) throws Throwable {
        long start = System.currentTimeMillis();
        try {
            return pjp.proceed();          // 执行原方法
        } finally {
            log.info("{} cost {}ms", pjp.getSignature(), System.currentTimeMillis() - start);
        }
    }
}

7.2 通知类型与执行顺序

@Around → @Before → 原方法 → @AfterReturning/@AfterThrowing → @After → @Around 收尾。

典型应用:接口日志、权限校验、分布式锁注解、方法限流、防重提交。

原理一句话:Spring 在 Bean 初始化后(postProcessAfterInitialization)扫描 @Aspect,为有切点匹配的 Bean 生成代理。调用方法时先进入代理→执行拦截链(通知)→最后 proceed 到目标方法。所以"同类内调用事务/切面失效"就是因为绕过了代理、直接 this.方法()。

7.3 自定义注解 + AOP 实战(分布式锁)

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DistributedLock {
    String key();                 // SpEL 表达式,如 "#orderId"
    long expire() default 30;     // 锁过期秒数
}

@Aspect @Component
public class LockAspect {
    @Around("@annotation(lock)")
    public Object around(ProceedingJoinPoint pjp, DistributedLock lock) throws Throwable {
        String realKey = parseSpel(lock.key(), pjp);     // 解析 SpEL 得到真实 key
        if (!redisLock.tryLock(realKey, lock.expire())) {
            throw new BizException("操作过于频繁");
        }
        try { return pjp.proceed(); }
        finally { redisLock.unlock(realKey); }
    }
}

8. 常用整合(starter 接入步骤)

组件 starter 要点
MyBatis-Plus mybatis-plus-boot-starter BaseMapper CRUD、分页插件、逻辑删除、自动填充
Spring Data JPA spring-boot-starter-data-jpa 方法名派生查询、审计字段
Redis spring-boot-starter-data-redis RedisTemplate + Jackson 序列化配置
Web spring-boot-starter-web 内嵌 Tomcat,HTTPS/压缩在 yaml 配
定时 spring-boot-starter-quartz 或 xxl-job 集群部署用 xxl-job 避免重复执行
文档 springdoc-openapi /swagger-ui 自动接口文档

8.1 RedisTemplate 序列化坑

@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
    RedisTemplate<String, Object> t = new RedisTemplate<>();
    t.setConnectionFactory(factory);
    // 默认 JDK 序列化会存乱码/二进制;生产用 JSON(Jackson)
    Jackson2JsonRedisSerializer<Object> json = new Jackson2JsonRedisSerializer<>(Object.class);
    t.setKeySerializer(RedisSerializer.string());          // key 用 String,可读
    t.setValueSerializer(json);                            // value 用 JSON
    t.setHashKeySerializer(RedisSerializer.string());
    t.setHashValueSerializer(json);
    return t;
}

8.2 MyBatis-Plus 分页

// 注册分页插件(否则 page 不生效,只是普通查询)
@Configuration
public class MybatisConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor i = new MybatisPlusInterceptor();
        i.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return i;
    }
}

// 使用
Page<Order> page = orderService.page(new Page<>(pageNum, pageSize),
        new LambdaQueryWrapper<Order>()
                .eq(Order::getUserId, userId)
                .ge(Order::getCreateTime, since)
                .orderByDesc(Order::getId));

8.3 springdoc-openapi(Swagger 替代)

springdoc:
  swagger-ui:
    path: /swagger-ui.html    # 访问路径
  api-docs:
    path: /v3/api-docs

安全提示:生产环境务必用 Spring Security 或网关把 /swagger-ui、/actuator 收敛,禁止公网暴露。

9. 工程结构与启动

mall-parent
├── mall-common          # 通用工具、统一返回、异常
├── mall-api             # Feign 接口 + DTO(供消费方依赖)
├── mall-user-service    # 用户服务(可独立部署)
├── mall-order-service   # 订单服务
└── mall-gateway         # 网关

打包与运行:

mvn clean package -DskipTests
java -jar -Xms1g -Xmx1g -Dspring.profiles.active=prod mall-order.jar

健康检查与指标:引入 spring-boot-starter-actuator,暴露 /actuator/health, /actuator/prometheus 给 K8s 探针与 Prometheus 抓取。

management:
  endpoints:
    web:
      exposure:
        include: health,info,prometheus,conditions   # conditions 用来查自动配置
  endpoint:
    health:
      show-details: when-authorized

10. 常见排错清单

现象 原因 / 排查
启动报 Consider defining a bean of type 'X' 没加 @Service/@Component,或主启动类扫描包不在其子包下(@SpringBootApplication 只扫当前及子包)
@Transactional 不回滚 方法非 public、同类内调用(this.)、抛的是受检异常未配 rollbackFor
循环依赖报错 构造器注入互相依赖;可用 @Lazy 破环或改为 setter 注入
配置项读不到 前缀拼错、忘记 @EnableConfigurationProperties、被更高优先级源覆盖
接口 404 路径/方法不匹配、Controller 没被扫描、@RestController 误写成 @Controller
返回中文乱码 produces = "application/json;charset=UTF-8" 或全局配置 MessageConverter 编码

11. 小结

  • IoC 把对象生命周期交给容器,Bean 经历"实例化→注入→初始化→就绪→销毁",后置处理器是 AOP/事务的织入点;
  • 自动配置 = @EnableAutoConfiguration 扫描 AutoConfiguration.imports + @Conditional 按需装配,覆盖默认靠自定义同型 Bean;
  • AOP 底层是 JDK 动态代理(有接口)或 CGLIB(无接口),同类调用绕过代理导致切面/事务失效;
  • 事务掌握七种传播行为与隔离级别,外部调用 + 正确 rollbackFor 是铁律;
  • 整合 starter 注意序列化(Redis)、分页插件(MP)、文档/监控的生产收敛。

至此 Java 基础与 Spring Boot 框架机制已串成线;前端部分见《01-前端Vue3》《02-Vue整合实操》。

posted @ 2026-09-19 05:40  Stellan7  阅读(6)  评论(0)    收藏  举报