Knowledge note
正在加载知识笔记
正在加载知识笔记
Knowledge note
模块定位:Spring Boot 应用启动与装配层
包路径:cn.bugstack/cn.bugstack.config
依赖关系:依赖所有其他模块(api, domain, infrastructure, trigger, types)
源码文件:11 个 Java 类 + 5 个资源配置文件
group-buy-market-app 是六层架构中的装配层。它不像 domain 那样包含业务逻辑,不像 api 那样定义接口契约,也不像 infrastructure 那样实现数据访问——它的职责只有一个:把所有的零件组装起来,启动应用。
┌─────────────────────────────────────────────────────────┐
│ app 模块 │
│ │
│ 1. 启动入口 Application.java (Spring Boot main) │
│ 2. 基础设施装配 线程池 / Redis / Guava / OkHttp / MQ │
│ 3. 配置管理 application.yml (多环境) │
│ 4. 横切关注点 TraceIdFilter (全链路追踪) │
│ │
└─────────────────────────────────────────────────────────┘
app 模块在 Maven 中依赖了所有其他模块,是整个项目的唯一可运行模块:
group-buy-market-app
├── group-buy-market-api (接口定义)
├── group-buy-market-domain (业务逻辑)
├── group-buy-market-infrastructure (数据访问实现)
├── group-buy-market-trigger (Controller/Job/Listener)
└── group-buy-market-types (公共类型)
这些依赖保证 Spring Boot 启动时能扫描到所有模块的 @Service、@Component、@Configuration。
| 维度 | domain | infrastructure | app |
|---|---|---|---|
| 有业务逻辑吗? | 有 | 无 | 无 |
| 定义接口吗? | 有(Repository/Port) | 无(实现接口) | 无 |
| 对接外部系统吗? | 不 | 对接(DB/Redis/MQ) | 不 |
| 是启动入口吗? | 不 | 不 | 是 |
| 做什么? | 造零件 | 造零件 | 组装 + 启动 |
group-buy-market-app/
├── pom.xml # 聚合依赖所有子模块
└── src/main/
├── java/cn/bugstack/
│ ├── Application.java # ★ Spring Boot 启动类
│ └── config/
│ ├── GuavaConfig.java # Google Guava 本地缓存
│ ├── OKHttpClientConfig.java # OkHttp HTTP 客户端
│ ├── RabbitMQConfig.java # RabbitMQ 交换机/队列绑定(已注释)
│ ├── RedisClientConfig.java # Redisson Redis 客户端
│ ├── RedisClientConfigProperties.java # Redis 配置属性
│ ├── ThreadPoolConfig.java # 线程池 Bean 定义
│ ├── ThreadPoolConfigProperties.java # 线程池配置属性
│ └── TraceIdFilter.java # ★ 全链路追踪 Filter
└── resources/
├── application.yml # 主配置(激活 dev 环境)
├── application-dev.yml # 开发环境配置(完整配置)
├── application-test.yml # 测试环境配置
├── application-prod.yml # 生产环境配置
└── logback-spring.xml # 日志配置
@SpringBootApplication
@Configurable
@EnableScheduling
public class Application {
public static void main(String[] args){
SpringApplication.run(Application.class);
}
}
三个注解的含义:
| 注解 | 作用 |
|---|---|
@SpringBootApplication | 组合注解:@Configuration + @EnableAutoConfiguration + @ComponentScan |
@Configurable | 允许 Spring 向非 Spring 管理的对象注入依赖(AOP 织入支持) |
@EnableScheduling | 启用定时任务(GroupBuyNotifyJob 和 TimeoutRefundJob 依赖此注解) |
@SpringBootApplication 默认从 Application.java 所在包 cn.bugstack 开始扫描。因为所有模块的包都遵循 cn.bugstack.xxx 命名:
cn.bugstack.api.xxx ← api 模块
cn.bugstack.domain.xxx ← domain 模块
cn.bugstack.infrastructure.xxx ← infrastructure 模块
cn.bugstack.trigger.xxx ← trigger 模块
cn.bugstack.types.xxx ← types 模块
所以一个 @SpringBootApplication 就能覆盖所有模块。
这是 app 模块最核心的配置之一,因为 domain 层的 MarketNode、结算回调、退单通知等都需要线程池。
@Data
@ConfigurationProperties(prefix = "thread.pool.executor.config", ignoreInvalidFields = true)
public class ThreadPoolConfigProperties {
private Integer corePoolSize = 20; // 核心线程数
private Integer maxPoolSize = 200; // 最大线程数
private Long keepAliveTime = 10L; // 空闲线程存活时间(秒)
private Integer blockQueueSize = 5000; // 阻塞队列容量
private String policy = "AbortPolicy"; // 拒绝策略
}
@Bean
@ConditionalOnMissingBean(ThreadPoolExecutor.class)
public ThreadPoolExecutor threadPoolExecutor(ThreadPoolConfigProperties properties) {
// 1. 根据配置选择拒绝策略
RejectedExecutionHandler handler;
switch (properties.getPolicy()){
case "AbortPolicy": // 抛异常
case "DiscardPolicy": // 静默丢弃
case "DiscardOldestPolicy": // 丢弃最老的
case "CallerRunsPolicy": // 调用者线程执行
}
// 2. 创建线程池
return new ThreadPoolExecutor(
properties.getCorePoolSize(),
properties.getMaxPoolSize(),
properties.getKeepAliveTime(),
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(properties.getBlockQueueSize()),
Executors.defaultThreadFactory(),
handler
);
}
@ConditionalOnMissingBean 的用途@ConditionalOnMissingBean(ThreadPoolExecutor.class)
这个注解的意思是:如果容器中还没有 ThreadPoolExecutor 类型的 Bean,才创建当前这个。这是一个防御性设计——如果将来有模块自定义了线程池,app 模块不会覆盖它。
贯穿整个 domain 层:
| 使用位置 | 用途 | 源码位置 |
|---|---|---|
MarketNode#multiThread() | 异步加载活动配置 + 商品信息 | activity/trial/node/MarketNode.java:66 |
TradeSettlementOrderService | 异步发送成团回调通知 | trade/settlement/TradeSettlementOrderService.java:83 |
AbstractRefundOrderStrategy | 异步发送退单回调消息 | trade/refund/business/AbstractRefundOrderStrategy.java:42 |
| 策略 | 行为 | 适用场景 |
|---|---|---|
AbortPolicy | 抛 RejectedExecutionException | 默认,需要感知任务提交失败 |
DiscardPolicy | 静默丢弃 | 不重要的任务(如日志) |
DiscardOldestPolicy | 扔队列头部的(最老的),再加新的 | 追求时效性 |
CallerRunsPolicy | 让提交任务的线程自己执行 | 开发环境(dev 配置使用此策略) |
dev 环境用 CallerRunsPolicy:开发时线程池满了不丢任务,由调用线程(通常是 Tomcat 线程)自己执行,虽然会阻塞请求但不会丢数据。
使用 Redisson 作为 Redis 客户端。
@Data
@ConfigurationProperties(prefix = "redis.sdk.config", ignoreInvalidFields = true)
public class RedisClientConfigProperties {
private String host;
private int port;
private int poolSize = 64;
private int minIdleSize = 10;
private int idleTimeout = 10000;
private int connectTimeout = 10000;
private int retryAttempts = 3;
private int retryInterval = 1000;
private int pingInterval = 0;
private boolean keepAlive = true;
}
@Bean("redissonClient")
@Primary
public RedissonClient redissonClient(..., RedisClientConfigProperties properties) {
Config config = new Config();
config.setCodec(JsonJacksonCodec.INSTANCE); // JSON 序列化
config.useSingleServer() // 单机模式
.setAddress("redis://" + properties.getHost() + ":" + properties.getPort())
.setConnectionPoolSize(properties.getPoolSize())
.setConnectionMinimumIdleSize(properties.getMinIdleSize())
.setIdleConnectionTimeout(properties.getIdleTimeout())
.setConnectTimeout(properties.getConnectTimeout())
.setRetryAttempts(properties.getRetryAttempts())
.setRetryInterval(properties.getRetryInterval())
.setPingConnectionInterval(properties.getPingInterval())
.setKeepAlive(properties.isKeepAlive());
return Redisson.create(config);
}
| 使用位置 | Redis 操作 | Key 示例 |
|---|---|---|
TeamStockOccupyRuleFilter | 组队库存抢占 | group_buy_market_team_stock_key_{activityId}_{teamId} |
| 降级/切量判断 | repository.downgradeSwitch() / cutRange() | 配置中心的开关 |
| xfg-wrench 配置中心 | 动态配置存储 | group-buy-market 相关 key |
static class RedisCodec extends BaseCodec {
// 使用 fastjson 做自定义序列化
// 注意:实际创建 Bean 时用的是 JsonJacksonCodec.INSTANCE,而不是这个
// 这个内嵌类是预留的备选方案
}
这个内嵌类虽然定义了,但当前 Bean 使用的是 JsonJacksonCodec.INSTANCE(Jackson 序列化)。RedisCodec 可以作为性能优化或兼容性需求的替代方案。
@Bean(name = "cache")
public Cache<String, String> cache() {
return CacheBuilder.newBuilder()
.expireAfterWrite(3, TimeUnit.SECONDS) // 写入后 3 秒过期
.build();
}
创建一个 Guava 本地缓存,用于临时数据。3 秒的过期时间说明它用于的是"短时间内防重复"的场景,比如:
与 Redis 的区别:Guava 是JVM 内存缓存(不跨实例),Redis 是分布式缓存(跨实例共享)。
@Bean
public OkHttpClient httpClient() {
return new OkHttpClient();
}
提供一个 OkHttpClient Bean,domain 层通过 ITradePort 接口的 infrastructure 实现类,使用这个 client 发起 HTTP 回调通知:
tradeTaskService.execNotifyJob()
→ port.groupBuyNotify(notifyTask)
→ OkHttpClient 发 POST 请求到 notifyUrl
配置极简(无超时、无连接池配置),生产环境可按需添加连接池、超时等参数。
//@Configuration ← 注意:整类已注释
public class RabbitMQConfig {
@Value("${spring.rabbitmq.config.producer.exchange}")
private String exchangeName; // "group_buy_market_exchange"
// 交换机:TopicExchange(支持 routing key 模糊匹配)
@Bean
public TopicExchange topicExchange() { ... }
// 队列 + 绑定:topic.team_success 路由键
@Bean
public Binding topicTeamSuccessBinding(...) { ... }
}
//@Configuration 被注释,说明当前的 MQ 配置用的是 Spring Boot 自动配置 + application-dev.yml 中的 RabbitMQ 配置项。这个类定义了更细颗粒度的交换机/队列/绑定,作为扩展预留。
spring:
rabbitmq:
config:
producer:
exchange: group_buy_market_exchange
topic_team_success:
routing_key: topic.team_success
queue: group_buy_market_queue_2_topic_team_success
topic_team_refund:
routing_key: topic.team_refund
queue: group_buy_market_queue_2_topic_team_refund
两个核心 MQ 主题:
topic.team_success:拼团成功通知(trigger 的 TeamSuccessTopicListener 消费)topic.team_refund:退单成功通知(trigger 的 RefundSuccessTopicListener 消费)@Component
public class TraceIdFilter extends OncePerRequestFilter {
private static final String TRACE_ID = "trace-id";
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain) {
try {
String traceId = UUID.randomUUID().toString();
MDC.put(TRACE_ID, traceId); // 写入 MDC
filterChain.doFilter(request, response);
} finally {
MDC.clear(); // 清理,防止内存泄漏
}
}
}
HTTP Request 进入
│
▼
TraceIdFilter.doFilterInternal()
│ 生成 UUID → 写入 MDC("trace-id", uuid)
▼
Controller → Service → Repository
│ 所有 log.info(...) 自动带上 [trace-id:xxx]
▼
TraceIdFilter 返回前 → MDC.clear()
MDC 是 SLF4J 提供的线程级上下文容器。MDC.put(key, value) 后,当前线程的所有日志都会带上这个 key-value。
在 logback-spring.xml 中配置日志格式:
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{trace-id}] %-5level %logger{36} - %msg%n
这样就能通过 trace-id 串联一次请求的所有日志,方便在 ELK/Grafana 中排查问题。
OncePerRequestFilter 的优势继承 OncePerRequestFilter(而非普通 Filter),保证:
application.yml → 指定 active profile = dev
application-dev.yml → 开发环境(完整本地配置)
application-test.yml → 测试环境
application-prod.yml → 生产环境
| 配置块 | 关键项 | 用途 |
|---|---|---|
server.tomcat | max-connections=20, threads.max=20 | Tomcat 线程池(dev 小配置) |
thread.pool.executor | core=20, max=50, CallerRunsPolicy | 业务线程池 |
spring.datasource | HikariCP, max-pool-size=25 | MySQL 连接池 |
spring.rabbitmq | 交换机/队列/路由键 | MQ 消息投递 |
redis.sdk.config | Redisson 单机模式 | Redis 分布式锁/缓存 |
xfg.wrench.config | 配置中心连接 | xfg-wrench 框架(决策树/责任链/动态配置) |
mybatis | mapper XML 路径 | MyBatis SQL 映射 |
logging | root=info, logstash | 日志级别 + 远程日志 |
management | Prometheus 启用 | 监控指标暴露 |
xfg:
wrench:
config:
system: group-buy-market
register:
host: 192.168.1.108
port: 16379
xfg-wrench 是项目自研框架,提供:
AbstractMultiThreadStrategyRouter(试算流程的基础)BusinessLinkedList + LinkArmory(锁单/结算/退单过滤链)app 模块通过这段 YAML 配置告诉 wrench 框架去哪里读写动态配置。
┌───────────────────┐
│ 外部调用方 │
└────────┬──────────┘
│ RPC / HTTP
┌────────▼──────────┐
│ trigger 模块 │ ← Marshall HTTP 请求
│ (Controller) │
└────────┬──────────┘
│ implements
┌────────▼──────────┐
│ api 模块 │ ← 定义接口契约
└────────┬──────────┘
│ uses
┌────────▼──────────┐
│ domain 模块 │ ← 执行业务逻辑
└────────┬──────────┘
│ calls interface
┌────────▼──────────┐
│ infrastructure │ ← 落地数据/缓存/消息
└────────┬──────────┘
│
┌────────▼──────────┐
│ types 模块 │ ← 通用枚举/异常
└───────────────────┘
┌────────────────────────────────────┐
│ ★ app 模块 ★ │
│ ┌──────────────────────────┐ │
│ │ Application.java (启动) │ │
│ │ Config 类 (装配 Bean) │ │
│ │ TraceIdFilter (全链路追踪) │ │
│ │ application.yml (配置) │ │
│ └──────────────────────────┘ │
│ 横向装配所有模块,启动整个 Spring │
└────────────────────────────────────┘
app 不是纵向的一层,而是横向的"胶水"——它通过 Spring 的依赖注入,把 api、domain、infrastructure、trigger、types 五个模块编织成一个可运行的应用程序。
很多初学者会把逻辑写在 app 模块,因为它离 main 近、离配置文件近。但在 DDD 中:
判断一段代码是否属于 app 模块的标准:"这段代码能不能脱离 Spring Boot 独立存在?" 如果能,就不该放在 app。
@ConfigurationProperties 的模式这个模块展示了配置管理的标准模式:
application-dev.yml
│
│ @ConfigurationProperties(prefix = "thread.pool.executor.config")
▼
ThreadPoolConfigProperties (POJO,带默认值)
│
│ 注入
▼
ThreadPoolConfig.threadPoolExecutor(properties)
│
│ @Bean
▼
ThreadPoolExecutor Bean → 被 domain 层 @Resource 注入使用
三步走:定义 Properties 类 → 创建 Bean → 业务代码注入使用。
@ConditionalOnMissingBean 的防御性思维@Bean
@ConditionalOnMissingBean(ThreadPoolExecutor.class)
public ThreadPoolExecutor threadPoolExecutor(...) { ... }
这不是废话——它表达了一个设计意图:"我提供一个默认实现,但允许其他模块覆盖。"这是框架级代码的习惯:给使用者留后路。
try {
MDC.put(TRACE_ID, traceId);
filterChain.doFilter(request, response);
} finally {
MDC.clear();
}
finally 中的 MDC.clear() 很重要。Tomcat 使用了线程池,请求结束后线程会被复用。如果不清理 MDC,下一个请求可能读到上一个请求的 trace-id,导致日志串联错乱。
application.yml → spring.profiles.active: dev
application-dev.yml → 完整的本地开发配置
application-test.yml → 测试环境(可能缺省)
application-prod.yml → 生产环境(可能缺省)
多环境配置的核心原则:公共配置放 application.yml,环境特有配置放 application-{profile}.yml,后者覆盖前者。
当前项目只在 application-dev.yml 写了完整配置,test 和 prod 空着——实际部署时会通过配置中心(如 Apollo、Nacos)或环境变量覆盖。
下一篇建议阅读:
group-buy-market-trigger模块 —— 了解 HTTP Controller、定时任务、MQ Listener 如何成为系统的外部入口。
知识笔记会随着实践和认知变化持续更新,不代表最终结论。