Back to Article

01. 面向微服务的 Spring 基础

01. 面向微服务的 Spring 基础

这一章补齐 Spring 的底层理解。读者已经会写 Spring MVC/Spring Boot 接口,但如果不理解容器、代理、配置和事务,进入 SOFA、Spring Cloud、金融交易链路后会频繁踩坑。

1. Spring 到底解决什么问题

Spring 的核心不是 @RestController,而是一个应用装配和运行时扩展体系。

在传统 Java 代码中,你可能这样写:

public class TransferService {
    private final AccountRepository accountRepository = new JdbcAccountRepository();
    private final RiskClient riskClient = new HttpRiskClient();
}

问题是:

  • 业务类自己创建依赖,无法替换实现。
  • 测试时很难注入 fake/mock。
  • 日志、事务、鉴权、指标等横切逻辑会散落在业务代码中。
  • 不同环境的配置要改代码。

Spring 的做法是:

  • 对象由容器创建和管理。
  • 依赖由容器注入。
  • 横切逻辑由 AOP、Filter、Interceptor、BeanPostProcessor 等机制统一挂载。
  • 配置从代码中分离出来。

用一句工程化的话概括:Spring 把“对象怎么创建、依赖怎么连接、运行期怎么增强”从业务代码里拿出来,让业务代码更专注于业务规则。

2. IoC/DI:容器与依赖注入

2.1 Bean 是什么

Bean 是被 Spring 容器管理的对象。一个类不是天然的 Bean,只有被容器识别、实例化、注册之后才是 Bean。

常见注册方式:

@Component
public class PaymentService {}
@Configuration
public class ClientConfig {
    @Bean
    RiskClient riskClient(RestTemplateBuilder builder) {
        return new RiskClient(builder.build());
    }
}
@Service
public class TransferService {
    private final AccountRepository accountRepository;

    public TransferService(AccountRepository accountRepository) {
        this.accountRepository = accountRepository;
    }
}

建议优先使用构造器注入。原因:

  • 依赖不可变,字段可以 final
  • 单元测试时可以直接 new。
  • 启动阶段就能发现缺少依赖。
  • 比字段注入更容易理解依赖关系。

2.2 容器装配流程

简化理解:

  1. 扫描配置类、组件注解和自动配置。
  2. 生成 BeanDefinition,也就是“如何创建 Bean”的元数据。
  3. 处理条件装配、Profile、属性绑定。
  4. 实例化 Bean。
  5. 注入依赖。
  6. 执行初始化回调和 BeanPostProcessor。
  7. 应用启动完成,Bean 对外提供能力。

理解这个流程后,很多问题会变清楚:

  • @Value 为什么读不到某个配置。
  • @Autowired 为什么出现多个候选 Bean。
  • @Transactional 为什么在同类内部调用时失效。
  • @ConfigurationProperties 为什么没绑定成功。

3. Spring Boot:不是“新框架”,而是约定和自动配置

Spring Boot 主要提供:

  • starter 依赖组合。
  • 自动配置。
  • 外部化配置。
  • 嵌入式 Web 容器。
  • Actuator 运维端点。
  • 测试支持。

3.1 Starter 的意义

spring-boot-starter-web 不是一个神秘框架,而是一组常用依赖和自动配置入口。它把 Web 开发常用的 Spring MVC、Jackson、嵌入式容器等组合起来。

金融微服务中,starter 的价值在于统一团队默认能力。例如可以做内部 starter:

  • 统一日志 traceId。
  • 统一异常响应。
  • 统一审计拦截器。
  • 统一鉴权客户端。
  • 统一 metrics tag。
  • 统一 Feign/RPC 超时。

但 starter 不能变成“黑盒魔法”。每个 starter 都应该有说明文档、默认值、覆盖方式和测试。

3.2 自动配置的核心模型

自动配置大致可以理解为:

@AutoConfiguration
@ConditionalOnClass(DataSource.class)
@ConditionalOnMissingBean(DataSource.class)
public class DataSourceAutoConfiguration {
    @Bean
    DataSource dataSource(DataSourceProperties properties) {
        return createDataSource(properties);
    }
}

关键点:

  • 条件装配不是强制装配。
  • 用户自己定义 Bean 时,自动配置通常会退让。
  • 自动配置依赖 classpath 和配置属性。
  • 排查问题时要看条件是否命中。

学习建议:

  1. 先会用 starter。
  2. 再学 @ConditionalOnClass@ConditionalOnMissingBean@ConfigurationProperties
  3. 最后写一个内部 starter,把日志、审计或统一响应封装进去。

4. 配置体系:微服务的运行时入口

微服务最大的区别之一是同一份代码会运行在本地、测试、预发、生产、灰度等环境。配置必须外置。

常见配置来源:

  • application.yml
  • application-dev.yml
  • 环境变量
  • 命令行参数
  • 配置中心,例如 Spring Cloud Config、Nacos
  • Kubernetes ConfigMap/Secret

推荐模式:

server:
  port: ${SERVER_PORT:8080}

payment:
  risk:
    endpoint: ${RISK_ENDPOINT:http://localhost:8081}
    timeout-ms: ${RISK_TIMEOUT_MS:800}
@ConfigurationProperties(prefix = "payment.risk")
public class RiskProperties {
    private URI endpoint;
    private Duration timeout = Duration.ofMillis(800);
}

不要把配置散落在 @Value("${...}") 中。结构化配置的好处:

  • 可以校验。
  • 可以生成元数据。
  • 可以在测试中整体替换。
  • 可以清晰表达一个外部依赖的全部参数。

5. AOP 与代理:事务、审计、指标的基础

AOP 适合处理横切关注点:

  • 事务。
  • 方法级审计。
  • 访问控制。
  • 统一日志。
  • 指标计时。
  • RPC 过滤器思想中的前后处理。

Spring AOP 通常基于代理。重要限制:

  • 同一个类内部 this.method() 调用不会经过代理。
  • private 方法无法被代理增强。
  • final 类/方法在某些代理模式下不能增强。
  • 代理增强发生在 Bean 容器管理对象上,自己 new 出来的对象不会生效。

事务失效示例:

@Service
public class TransferService {
    public void transfer() {
        this.debit(); // 同类内部调用,不经过事务代理
    }

    @Transactional
    public void debit() {
        // update account
    }
}

解决方式:

  • 把事务边界放在外部入口方法上。
  • 拆到另一个 Bean。
  • 避免把事务注解当作“随手贴”的标记。

6. 事务:先掌握本地事务,再谈分布式事务

本地事务关注一个数据库连接内的原子性。常见写法:

@Transactional
public void createPayment(CreatePaymentCommand command) {
    paymentRepository.insert(command.toPayment());
    paymentEventRepository.insert(command.toEvent());
}

常见误区:

  • 捕获异常后不抛出,事务不会按预期回滚。
  • 默认只对 unchecked exception 回滚。
  • 事务方法内部调用导致注解不生效。
  • 事务里做远程调用,导致锁持有时间不可控。
  • 大事务包含批量处理,影响数据库吞吐。

金融微服务建议:

  • 本地事务只包住本地数据变更。
  • 远程调用放在事务外,或用 Outbox/事务消息。
  • 明确幂等键,避免重试导致重复写。
  • 每个交易状态转换写业务流水。

7. Actuator 与可观测性

Spring Boot Actuator 提供健康检查、指标等运维入口。生产微服务至少要关注:

  • /actuator/health:是否可接流量。
  • readiness/liveness:在 Kubernetes 中区分“能启动”和“能接请求”。
  • metrics:请求耗时、错误数、线程池、连接池、JVM。
  • info:版本、commit、构建时间。

不要把 Actuator 全量暴露到公网。建议:

  • 运维端点走内网。
  • 敏感端点加鉴权。
  • 自定义健康检查要区分关键依赖和非关键依赖。
  • DB 不可用时,支付写接口应拒绝;短信服务不可用时,部分查询接口可能仍可服务。

8. 学习练习

练习 1:解释一个接口从请求到响应的路径

写一个 POST /transfers 接口,然后画出:

  1. Servlet 容器接收请求。
  2. Filter 处理 traceId。
  3. Spring MVC 找到 Controller。
  4. 参数绑定和校验。
  5. Service 执行业务。
  6. Repository 写数据库。
  7. 事务提交。
  8. Controller 返回响应。
  9. 统一异常处理。
  10. Actuator/Micrometer 记录指标。

练习 2:做一个内部 starter

目标:封装统一响应和 traceId 日志。

要求:

  • starter 自动注册一个 OncePerRequestFilter
  • 从请求头读取 X-Request-Id,没有则生成。
  • 日志 MDC 写入 requestId。
  • 响应头带回 X-Request-Id
  • 提供配置 company.trace.enabled=false 可关闭。

练习 3:事务失效实验

构造同类内部调用、异常被吞、checked exception 三种事务失效情况,写测试证明差异。

9. 本章检查清单

  • 能解释 IoC 和 DI,不只会说“控制反转”四个字。
  • 能解释 Bean 生命周期中配置、实例化、注入、初始化的大致顺序。
  • 能说明 Spring Boot starter 和自动配置的关系。
  • 能用 @ConfigurationProperties 管理一组配置。
  • 能解释 @Transactional 的代理限制。
  • 能解释 Actuator health、metrics 与 Kubernetes probe 的关系。