关键在于用面向领域的存储接口抽象介质差异,如orderstorage、attachmentstore,参数返回值均为领域对象;实现层透明适配jdbc/mybatis/事件溯源;租户上下文自动注入并强制校验;副作用与约束通过契约声明而非业务处理。

关键不在“加一层接口”,而在于让业务代码彻底感知不到存储介质的存在——接口要成为契约,不是胶水。
定义面向领域而非技术的存储接口
不要写 MySQLOrderRepository 或 RedisCacheService 这类带技术名词的接口名。应聚焦业务语义:
-
OrderStorage:只声明save(Order)、findById(OrderId)、findByCustomerId(CustomerId) -
AttachmentStore:只暴露upload(Attachment)、download(AttachmentId)、delete(AttachmentId) - 所有方法参数和返回值用领域对象(如
OrderId、Attachment),禁止传String sql或byte[] data
实现层做存储介质的透明适配
同一接口可有多个实现,运行时由配置或上下文决定使用哪一种,业务代码无感知:
-
JdbcOrderStorage:基于 JDBC + 多租户插件,自动注入tenant_id条件 -
MyBatisOrderStorage:用 MyBatis-Plus 的TenantLineInnerInterceptor实现逻辑隔离 -
EventSourcedOrderStorage:将操作转为事件存入 Kafka,读取走物化视图 - 切换只需改 Spring Bean 注册方式(
@Profile("jdbc")或动态路由),不碰业务逻辑
强制租户上下文穿透存储链路
接口本身不接收 tenantId 参数,但实现必须从当前线程上下文提取:
- 使用
ThreadLocal<tenantcontext></tenantcontext>或 Spring 的RequestContextHolder获取认证后的租户标识 - 所有数据库查询自动追加
WHERE tenant_id = ?;对象存储路径强制拼接/tenant_abc123/files/... - 缓存 key 统一格式:
order:tenant_abc123:12345,避免跨租户误读 - 若上下文为空,直接抛
IllegalStateException("Missing tenant context"),不降级或默认值
用抽象封装存储副作用与边界约束
把介质特性转化为接口契约的一部分,而不是让业务去处理:
-
AttachmentStore.upload()方法契约中明确:成功后返回可公开访问的 HTTPS URL,且该 URL 自动携带租户前缀校验逻辑 -
OrderStorage.findById()契约注明:若 ID 存在但不属于当前租户,行为是返回null(非抛异常),业务层无需额外判空租户 - 对强一致性要求高的操作(如扣减库存),接口方法标注
@StrongConsistency注解,实现类据此选择事务型存储
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











