一、力扣
1、圆环回原点
import java.util.*;
public class Solution {
public int circle (int n) {
int[][] dp = new int[n + 1][10];
int MOD = 1000000007;
dp[0][0] = 1;
for (int i = 1; i <= n; i++) {
for (int j = 0; j < 10; j++) {
dp[i][j] = (dp[i - 1][(j - 1 + 10) % 10] + dp[i - 1][(j + 1) % 10]) % MOD;
}
}
return dp[n][0];
}
}
2、前 K 个高频元素
class Solution {
public int[] topKFrequent(int[] nums, int k) {
Map<Integer,Integer> map=new HashMap<>();
for(var x:nums){
map.merge(x,1,Integer::sum);
}
int n=Collections.max(map.values());
List<Integer>[] list=new List[n+1];
Arrays.setAll(list,e->new ArrayList<>());
for(Map.Entry<Integer,Integer> entry:map.entrySet()){
list[entry.getValue()].add(entry.getKey());
}
int[] res=new int[k];
int point=0;
for(int i=n;i>0;i--){
for(var x:list[i]){
if(point<k) res[point++]=x;
}
}
return res;
}
}
class Solution {
public int[] topKFrequent(int[] nums, int k) {
Map<Integer, Integer> map = new HashMap<>();
for (int x : nums) {
map.merge(x, 1, Integer::sum);
}
PriorityQueue<int[]> queue = new PriorityQueue<>((a, b) -> a[0] - b[0]);
for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
if (queue.size() == k) {
if (entry.getValue() > queue.peek()[0]) {
queue.poll();
queue.offer(new int[] { entry.getValue(), entry.getKey() });
}
} else {
queue.offer(new int[] { entry.getValue(), entry.getKey() });
}
}
int[] ret = new int[k];
for (int i = 0; i < k; ++i) {
ret[i] = queue.poll()[1];
}
return ret;
}
}
二、面试
1、 加分布式滑块锁的意义,能否加库存,加库存后如何处理?
1. 加分布式滑块锁的意义
再细化一下,多个用户过来请求库存值,是不做任何加锁操作直接读取出来对吗?对。 追问:decr已经是原子操作了「多个请求一定是串行的」,你为什么还要加setnx锁?「是否有必要加这个锁」 加锁的意义是什么,不加这个锁又会出现什么问题?
回答:其实本身原本的库存,扣减最早的方式是数据库行级锁,数据库扛不住。优化到redis缓存,使用了redis独占锁,但这样会出现排队问题,导致有库存,但吞吐量不佳。后来设计为颗粒度更细的无锁化(乐观锁)设计。incr/decr 是原子操作,基本也不会出问题,这个操作就有点像数据库计数,1、2、3、4... 而加锁类似写了流水记录。相对来说这样的方式更可靠。避免在;集群配置、网络、库存恢复、人工调整等场景时,导致 incr/decr 的值不对。因为如果不加锁,这个时候不对,是不知道的。不知道就很可怕,哪怕有万分之一的概率,只要有足够多的用户参与,也会出问题。(尤其运营配置,运营是很多人,也有很多新人,实际公司里很多事故都是运营配置导致的,而且出问题的时候是一堆人围着一个运营,运营很慌,配置错的概率更大)
2. 能否加库存,加库存后如何处理
假设我当前活动奖品A还要十个库存,运营想再加十个库存,你这个时候是怎么处理的,支持吗? 追问:假设我再增加一次库存,或者n次库存,你是怎么处理的呢?「运营经常对库存做一些操作」 我的回答:活动降级补充库存?或者配置多个库存池,或者用incr+消息队列去补充库存
回答:可以先调整数据库库存,之后使用 incrby 给缓存加库存。这个时候要注意使用 incr 和总量对比。如果有对库存的失败记录,可以用 incr 和 总量 + 恢复了对比。 加锁,用于兜底;集群配置、网络、库存恢复、人工调整等。尽量避免出问题,加库存就是其中的一个。
2、 项目的亮点或较难的地方在哪里?
2.1 领域驱动设计(DDD)架构
项目采用了 DDD 架构,将系统分为多个模块:
big-market-api: 对外接口层big-market-app: 应用层big-market-domain: 领域层big-market-infrastructure: 基础设施层big-market-trigger: 触发器层big-market-types: 类型定义层
这种分层架构使得系统职责清晰,便于维护和扩展。
2.2 责任链模式实现抽奖规则
从代码中可以看到使用了责任链模式(如 BackListLogicChain 类)来处理不同的抽奖规则,这使得规则可以灵活组合和扩展:
- 黑名单规则链
- 权重规则链
- 锁定规则链(通过抽奖次数解锁)
2.3 库存管理的异步处理
项目中实现了奖品库存的异步更新机制:
- 使用 Redis 缓存库存信息
- 通过延迟队列异步更新数据库库存
- 定时任务
UpdateAwardStockJob处理库存更新
这种设计避免了直接对数据库的频繁操作,提高了系统性能。
2.4 事件发布订阅模式
项目使用了事件发布订阅模式处理业务流程:
EventPublisher负责消息发送- 使用 RabbitMQ 作为消息队列
- 通过
TaskEntity记录消息发送状态
2.5 消息补偿机制
实现了消息发送失败的补偿机制:
- 消息发送状态记录
- 失败任务的重试机制
- 事务外执行消息发送,保证数据一致性
2.6 分库分表策略
项目使用了自定义的数据库路由策略:
@DBRouter注解标记需要路由的方法IDBRouterStrategy接口定义路由策略- 基于用户ID进行分库分表
2.7 完善的单元测试
项目包含了丰富的单元测试,如 RaffleStrategyTest、AwardDaoTest 等,确保各模块功能正常。
2.8 容器化部署
项目支持 Docker 容器化部署:
-
包含 Dockerfile 和构建脚本
-
docker-compose 配置文件支持环境依赖(MySQL、Redis、RabbitMQ)的快速部署
3、 锁机制是如何实现的?(回答:使用Redis做次数锁)
decr预扣减+setnx分段锁
4、 布隆过滤器的底层实现逻辑是什么?如何减小误判误差?
对数据进行二次查询。
布隆过滤器基于位数组和多个独立哈希函数。元素插入时,通过多个哈希函数映射到位数组的多个位置并置1;查询时检查所有位置是否全为1,若否则元素一定不存在,若是则可能存在(误判)。减小误判误差的方法:
- 增大位数组大小:减少哈希碰撞概率。
- 增加哈希函数数量:提高分布均匀性,但会增加计算开销。
- 选择低冲突的哈希函数:如MurmurHash等非加密型哈希。
- 结合其他数据结构(如布谷鸟过滤器)或定期重建过滤器,但需权衡空间与时间效率。
5、 JVM对象创建的完整生命周期是怎样的?(涉及类加载、内存分配、垃圾回收等过程)
6、 双亲委派机制是什么?有什么好处?
7、 常见的垃圾回收算法有哪些?各自适用场景和原理是什么?
- 标记-清除:标记无用对象后清除,产生内存碎片,适用于老年代(如CMS的并发标记清除)。
- 复制算法:将内存分为两块,存活对象复制到另一块后清理旧块,适用于新生代(如Serial/ParNew)。
- 标记-整理:标记后存活对象向一端移动,清理边界外内存,避免碎片,适用于老年代(如Parallel Old)。
- 分代收集:结合新生代(复制)和老年代(标记-整理/CMS),适应不同对象存活率。
- 增量收集(如G1):分区管理,可预测停顿,适用于大堆内存和低延迟场景。
8、 JVM运行时数据区的各个部分及其作用是什么?(包括本地方法栈等)
- 程序计数器:线程私有,记录当前线程执行的字节码行号(用于分支、循环等控制)。
- Java虚拟机栈:线程私有,存放方法栈帧(局部变量、操作数栈、动态链接等)。
- 本地方法栈:为Native方法服务,与虚拟机栈类似。
- 堆:线程共享,存放对象实例和数组,GC主要管理区域(分新生代、老年代)。
- 方法区(元空间):存储类元信息、常量、静态变量,JDK8后使用本地内存。
- 直接内存(堆外内存):通过NIO的ByteBuffer分配,减少堆内存拷贝,但受操作系统限制。
9、 JNI相关知识。如何用C语言实现比Java更高效的算法?(涉及JNI相关知识)
JNI允许Java调用本地(C/C++)代码,提升性能的场景:
-
密集计算:如数学运算(FFT)、图像处理,C可直接操作内存,避免JVM解释开销。
-
系统级调用:如文件IO、网络协议栈,绕过Java安全检查。
-
优化技巧:
- 减少JNI方法调用次数(批量处理数据)。
- 使用
GetPrimitiveArrayCritical直接访问数组内存,避免复制。 - 避免频繁创建/销毁本地引用,使用全局引用缓存对象。
示例:用C实现矩阵乘法,通过指针操作连续内存,比Java双重循环快10~100倍。
三、面试场景题
1、在海量用户场景下,如何判断一个用户是否抽过奖?
在海量用户场景下判断用户是否抽过奖,可采用布隆过滤器实现空间高效的概率判断,Redis位图存储用户状态实现高性能查询,分库分表+索引方案保障数据持久化,或结合多种技术的混合方案,通过多层缓存设计兼顾性能与可靠性,满足海量用户高并发抽奖场景需求。
1. 布隆过滤器方案
布隆过滤器是一种空间效率极高的概率型数据结构,特别适合判断"是否存在"的场景。
工作原理:
- 使用多个哈希函数将用户ID映射到位数组的不同位置
- 查询时,如果对应位置都为1,则可能存在;如果有一个为0,则一定不存在
- 内存占用极小,10亿用户只需约1.8GB内存(假设1%误判率)
优势:
- 查询和写入都是O(1)时间复杂度,性能极高
- 内存占用极小,适合海量数据
- 可以分布式部署
局限性:
- 有一定误判率(只会误判为"存在",不会误判为"不存在")
- 不支持删除操作
2. Redis位图方案
利用Redis的Bitmap数据结构,每个用户占用1位。
实现方式:
- 使用
SETBIT key offset 1标记用户已抽奖 - 使用
GETBIT key offset查询用户是否抽过奖 - 可按活动ID或日期分开存储
优势:
- 极高的空间效率,1亿用户仅需约12MB内存
- 查询性能高,O(1)复杂度
- 支持持久化和分布式部署
适用场景:
- 用户ID为连续数字或可映射为连续数字
- 单个活动参与用户量在亿级以下
3. 分库分表+数据库索引方案
对于需要持久化且有复杂查询需求的场景。
实现方式:
- 按用户ID哈希分库分表
- 在用户ID和活动ID上建立联合索引
- 使用读写分离提高查询性能
优势:
- 数据持久化,不会丢失
- 支持复杂查询和统计
- 可靠性高
局限性:
- 查询性能比内存方案低
- 系统复杂度高
- 存储成本高
2、如何统计每天用户的抽奖按钮点击次数?
统计每天用户抽奖按钮点击次数可通过Redis实时计数器实现高性能实时统计,日志收集+离线分析方案支持深度多维分析,流式计算框架实现准实时统计处理,或采用混合架构方案结合实时计数与批处理分析,通过双写架构保障数据一致性,同时支持多维度分析与异常监控。
1. 实时计数器方案
使用Redis实现高性能实时计数。
实现方式:
- 使用
INCR命令增加计数 - 按日期和用户ID组织键名:
clicks:YYYY-MM-DD:userId - 设置TTL自动过期(如保留30天)
数据结构设计:
- 用户维度:
clicks:date:userId→ 计数值 - 全局维度:
clicks:date:total→ 总计数值 - 可使用Hash结构优化内存:
HINCRBY clicks:date userId 1
优势:
- 实时性高,写入和查询都是O(1)
- 支持多维度统计
- 自动过期机制节省存储空间
2. 日志收集+离线分析方案
适合需要深度分析的场景。
实现流程:
- 记录点击事件日志(包含用户ID、时间戳、事件类型等)
- 使用日志收集系统(如ELK、Flume)收集日志
- 通过Hadoop/Spark等进行离线分析
- 生成报表并存储结果
优势:
- 支持复杂的多维分析
- 可追溯历史数据
- 可与其他业务数据关联分析
局限性:
- 实时性较差
- 系统复杂度高
- 存储和计算成本高
3. 流式计算方案
使用流计算框架实现准实时统计。
技术选型:
- Flink/Spark Streaming等流计算框架
- Kafka作为消息队列
- Redis/HBase存储统计结果
实现流程:
- 点击事件发送到Kafka
- 流计算作业消费消息并进行窗口计算
- 结果写入存储系统
- 提供API查询统计结果
优势:
- 准实时性(秒级延迟)
- 支持复杂的时间窗口计算
- 可扩展性好,适合大规模数据
4. 混合方案(推荐)
结合实时计数和批处理分析:
架构设计:
- 前端埋点收集点击事件
- 双写架构:
- 写入Redis进行实时计数
- 写入Kafka用于后续分析
- 定时任务将Redis数据持久化到数据库
- 流计算作业处理Kafka数据,生成多维度统计结果
数据一致性保障:
- 使用分布式事务或最终一致性方案
- 定期对账和修正数据偏差
扩展能力:
- 支持按时间、地域、用户群体等多维度分析
- 支持异常检测和实时告警
3、如何统计API接口的调用耗时?
API接口调用耗时统计可通过代码埋点(AOP切面、过滤器、注解)实现细粒度监控,专业APM工具提供全面监控与可视化分析,分布式追踪系统实现微服务架构下的全链路分析,或构建多层次监控体系,从基础设施层到业务层全面覆盖,通过采集-存储-分析-可视化-告警的完整流程,及时发现并解决性能问题。
1. 代码埋点方案
在代码层面实现耗时统计。
实现方式:
-
AOP切面:
- 定义切面拦截所有API方法
- 记录方法执行前后的时间差
- 支持细粒度控制和自定义标签
-
过滤器/拦截器:
- 在请求处理前后记录时间
- 适合Web应用,可获取HTTP相关信息
- 实现简单,对代码侵入性低
-
注解方式:
- 自定义注解标记需要监控的方法
- 结合AOP实现,灵活性高
- 可按业务场景分类统计
数据处理:
- 本地聚合后定期上报
- 使用异步方式避免影响主流程
- 设置采样率减少数据量
2. 监控系统方案
使用专业的APM(应用性能监控)工具。
主流工具:
- Prometheus + Grafana:开源监控系统,支持自定义指标和告警
- SkyWalking:分布式追踪系统,支持全链路分析
- Pinpoint:细粒度性能分析工具
- Datadog/New Relic:商业APM解决方案
实现方式:
- 集成监控SDK到应用中
- 配置监控指标和采样规则
- 设置可视化面板和告警规则
优势:
- 开箱即用,功能全面
- 支持多维度分析和可视化
- 有完善的告警机制
3. 分布式追踪方案
适合微服务架构下的全链路监控。
技术选型:
- Jaeger/Zipkin:开源分布式追踪系统
- OpenTelemetry:可观测性标准
- SkyWalking:全栈监控平台
核心概念:
- Trace:一次完整的请求调用链
- Span:调用链中的一个操作单元
- Context Propagation:上下文传播机制
实现流程:
- 在服务入口创建Trace
- 在每个调用点创建Span
- 通过上下文传播机制关联Span
- 收集Trace数据并分析
优势:
- 支持全链路分析
- 可视化调用关系
- 精准定位性能瓶颈
4. 综合解决方案(推荐)
多层次监控体系:
监控层次:
-
基础设施层:
- 服务器CPU、内存、网络监控
- 数据库性能监控
- 中间件监控
-
应用层:
- API响应时间监控
- 错误率监控
- JVM/容器资源监控
-
业务层:
- 核心业务指标监控
- 用户体验监控
- 业务异常监控
数据处理流程:
- 采集:多种方式收集性能数据
- 存储:时序数据库存储监控数据
- 分析:统计分析和异常检测
- 可视化:直观展示性能指标
- 告警:及时发现并通知异常
最佳实践:
- 设置合理的采样率和聚合策略
- 建立基线和阈值,实现智能告警
- 关联业务指标,评估性能影响
- 定期优化和回顾
4、如何将大文件从一个数据库安全完整地转移到另一个数据库?(需考虑顺序性保障)
大文件从一个数据库安全完整转移到另一个数据库,可采用批量处理方案分批迁移减少资源占用,专业ETL工具提供可视化配置与监控,数据库原生工具实现高效同类型迁移,CDC方案支持最小停机时间的实时同步,或根据数据规模选择合适的综合方案,通过唯一标识、排序字段、检查点机制、事务完整性、数据验证、回滚机制和监控告警等措施保障数据迁移的顺序性与完整性。
1. 批量处理方案
通过分批处理减少资源占用和风险。
实现步骤:
-
数据分析:
- 分析数据量和结构
- 确定主键或唯一标识
- 评估依赖关系
-
分批策略:
- 按主键范围分批
- 每批大小根据系统性能调整(通常500-5000条)
- 保持每批数据的完整性
-
顺序保障:
- 使用有序字段(如自增ID、时间戳)
- 严格按顺序处理批次
- 记录处理进度和检查点
-
事务控制:
- 每批使用独立事务
- 设置合理的超时时间
- 实现幂等性处理
优势:
- 资源占用可控
- 支持断点续传
- 出错影响范围小
2. ETL工具方案
使用专业数据集成工具。
主流工具:
- Apache NiFi:可视化数据流管理
- Talend:企业级数据集成平台
- Informatica:商业ETL解决方案
- Kettle(Pentaho):开源ETL工具
实现流程:
- 配置源数据库和目标数据库连接
- 设计数据转换流程
- 配置错误处理和重试机制
- 设置监控和日志记录
- 执行并监控迁移过程
优势:
- 可视化配置,降低开发成本
- 内置各种数据源连接器
- 提供完善的监控和日志
3. 数据库原生工具方案
利用数据库自带的导入导出功能。
MySQL实现:
# 导出(保持顺序)
mysqldump --single-transaction --order-by-primary --where="1 order by id" -u user -p db_name table_name > data.sql
# 导入
mysql -u user -p target_db < data.sql
PostgreSQL实现:
# 导出
pg_dump -t table_name --data-only --column-inserts source_db > data.sql
# 导入
psql target_db < data.sql
优势:
- 工具成熟稳定
- 支持事务和一致性
- 适合同类型数据库迁移
4. CDC(变更数据捕获)方案
适合需要实时同步或最小停机时间的场景。
技术选型:
- Debezium:开源CDC平台
- Oracle GoldenGate:商业CDC解决方案
- AWS DMS:云平台数据迁移服务
- Canal:阿里开源MySQL binlog解析工具
实现原理:
- 捕获源数据库的变更日志(如binlog)
- 解析变更事件并转换为标准格式
- 按顺序应用到目标数据库
- 保证事务一致性和顺序性
优势:
- 最小停机时间
- 保证数据一致性
- 支持异构数据库迁移
综合解决方案(推荐)
根据数据规模和业务要求选择合适方案:
小规模数据(<10GB):
- 使用数据库原生工具一次性迁移
- 设置合理的事务大小和超时时间
- 迁移后进行数据校验
中等规模数据(10GB-100GB):
- 使用批处理方案分批迁移
- 实现检查点机制支持断点续传
- 并行处理无依赖的表
大规模数据(>100GB):
- 使用专业ETL工具或CDC方案
- 先迁移历史数据,再同步增量变更
- 实施分阶段迁移策略
顺序性保障关键措施:
- 唯一标识:确保每条记录有唯一标识
- 排序字段:使用自增ID或时间戳等有序字段
- 检查点机制:记录已处理位置
- 事务完整性:保证每批数据的事务完整性
- 数据验证:迁移后进行数据一致性校验
- 回滚机制:出错时能够安全回滚
- 监控告警:实时监控迁移进度和异常
迁移前准备工作:
- 评估数据量和结构
- 制定详细迁移计划
- 准备充足的存储空间
- 设置监控和告警
- 进行小规模测试验证
- 制定应急回滚方案
迁移后验证工作:
-
数据完整性校验
-
数据一致性校验
-
业务功能测试
-
性能测试
-
监控系统运行状态
5、项目中接口和抽象类的使用分析
一、接口(Interface)的使用
接口在项目中主要用于定义行为契约,实现依赖倒置原则,使高层模块不依赖于低层模块的实现细节。
1. 仓储接口
public interface IActivityRepository {
ActivitySkuEntity queryActivitySku(Long sku);
ActivityEntity queryRaffleActivityByActivityId(Long activityId);
// ... existing code ...
}
使用原因:
- 遵循领域驱动设计(DDD)思想,在领域层定义仓储接口,由基础设施层实现
- 实现依赖倒置,领域层不依赖于具体的数据访问技术
- 便于单元测试,可以通过Mock仓储接口进行测试
2. 服务接口
public interface IRaffleActivityService {
/**
* 活动装配,数据预热缓存
* @param activityId 活动ID
* @return 装配结果
*/
Response<Boolean> armory(Long activityId);
/**
* 活动抽奖接口
* @param request 请求对象
* @return 返回结果
*/
Response<ActivityDrawResponseDTO> draw(ActivityDrawRequestDTO request);
}
使用原因:
- 定义服务契约,明确服务边界
- 便于不同模块之间的集成
- 支持面向接口编程,降低系统耦合度
3. DAO接口
@Mapper
public interface IRaffleActivityAccountDao {
void insert(RaffleActivityAccount raffleActivityAccount);
int updateAccountQuota(RaffleActivityAccount raffleActivityAccount);
// ... existing code ...
}
使用原因:
- 与ORM框架(如MyBatis)集成,通过接口定义SQL操作
- 分离数据访问逻辑与业务逻辑
- 便于数据库操作的统一管理
4. 责任链接口
public interface ILogicChainArmory {
ILogicChain next();
ILogicChain appendNext(ILogicChain next);
}
使用原因:
- 定义责任链模式的标准行为
- 支持灵活的链式处理流程
- 便于扩展新的处理节点
二、抽象类(Abstract Class)的使用
抽象类在项目中主要用于提供基础实现,封装共同行为,同时允许子类定制特定行为。
1. 抽象策略类
@Slf4j
public abstract class AbstractRaffleStrategy implements IRaffleStrategy {
// 策略仓储服务
protected IStrategyRepository repository;
// 策略调度服务
protected IStrategyDispatch strategyDispatch;
// ... existing code ...
@Override
public RaffleAwardEntity performRaffle(RaffleFactorEntity raffleFactorEntity) {
// 1. 参数校验
// ... existing code ...
// 2. 责任链抽奖计算
// ... existing code ...
// 3. 规则树抽奖过滤
// ... existing code ...
// 4. 返回抽奖结果
return buildRaffleAwardEntity(strategyId, treeStrategyAwardVO.getAwardId(), treeStrategyAwardVO.getAwardRuleValue());
}
// 抽象方法,由子类实现
public abstract DefaultChainFactory.StrategyAwardVO raffleLogicChain(String userId, Long strategyId);
// ... existing code ...
}
使用原因:
- 实现模板方法模式,定义算法骨架,让子类实现特定步骤
- 封装共同的业务逻辑,减少代码重复
- 提供必要的抽象方法,强制子类实现特定行为
2. 抽象责任链
public abstract class AbstractActionChain implements IActionChain {
private IActionChain next;
@Override
public IActionChain next() {
return next;
}
@Override
public IActionChain appendNext(IActionChain next) {
this.next = next;
return next;
}
}
使用原因:
- 实现责任链模式的基础功能
- 封装链式调用的通用逻辑
- 让子类专注于实现具体的处理逻辑
3. 抽象活动配额服务
@Slf4j
public abstract class AbstractRaffleActivityAccountQuota extends RaffleActivityAccountQuotaSupport implements IRaffleActivityAccountQuotaService {
public AbstractRaffleActivityAccountQuota(IActivityRepository activityRepository, DefaultActivityChainFactory defaultActivityChainFactory) {
super(activityRepository, defaultActivityChainFactory);
}
@Override
public String createOrder(SkuRechargeEntity skuRechargeEntity) {
// 1. 参数校验
// ... existing code ...
// 2. 查询基础信息
// ... existing code ...
// 3. 活动动作规则校验
// ... existing code ...
// 4. 构建订单聚合对象
// ... existing code ...
// 5. 保存订单
// ... existing code ...
// 6. 返回单号
return createOrderAggregate.getActivityOrderEntity().getOrderId();
}
protected abstract CreateQuotaOrderAggregate buildOrderAggregate(SkuRechargeEntity skuRechargeEntity, ActivitySkuEntity activitySkuEntity, ActivityEntity activityEntity, ActivityCountEntity activityCountEntity);
protected abstract void doSaveOrder(CreateQuotaOrderAggregate createOrderAggregate);
}
使用原因:
- 实现模板方法模式,定义订单创建的标准流程
- 封装通用的业务逻辑和校验规则
- 通过抽象方法让子类实现特定的订单构建和保存逻辑
4. 抽象事件基类
@Data
public abstract class BaseEvent<T> {
public abstract EventMessage<T> buildEventMessage(T data);
public abstract String topic();
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public static class EventMessage<T> {
private String id;
private Date timestamp;
private T data;
}
}
使用原因:
- 定义事件的基本结构和行为
- 使用泛型支持不同类型的事件数据
- 强制子类实现特定的事件构建和主题定义方法
三、接口与抽象类的选择原则
在项目中,接口和抽象类的选择遵循以下原则:
-
使用接口的场景:
- 需要定义行为契约但不提供实现时
- 需要支持多重继承时
- 实现依赖倒置原则,解耦系统组件时
- 定义服务边界和API时
-
使用抽象类的场景:
- 需要提供部分实现,减少子类代码重复时
- 实现模板方法模式,定义算法骨架时
- 需要在相关类之间共享状态和行为时
- 需要定义受保护的方法和字段时
四、总结
在big-market项目中,接口和抽象类的使用体现了良好的面向对象设计原则:
-
接口主要用于定义契约、实现依赖倒置、支持多态,如仓储接口、服务接口等。
-
抽象类主要用于实现模板方法模式、封装共同行为、提供基础实现,如抽象策略类、抽象责任链等。
-
两者结合使用,构建了灵活、可扩展的系统架构,体现了"面向接口编程"和"组合优于继承"的设计思想。
这种设计方式使得系统具有良好的可维护性、可扩展性和可测试性,是Java企业级应用开发的最佳实践之一。