Core 核心模块
Core 模块是 sh-framework 框架的核心基础组件,提供应用程序开发所需的基础技术能力和通用架构组件。它封装了开发中常用的非业务性技术组件、基础实体、异常处理、响应封装、身份上下文等核心功能。
模块概述
核心价值
- 标准化:统一技术实现标准,提升代码一致性
- 高效性:减少重复代码编写,提高开发效率
- 可维护性:集中管理基础组件,便于维护和升级
- 扩展性:提供灵活的扩展机制,支持定制化需求
解决的问题
- 基础实体重复定义问题
- 响应格式不统一问题
- 异常处理混乱问题
- 用户身份上下文管理复杂问题
- 接口/字段描述信息缺失问题
核心功能特性
1. 基础实体类
- DbColumnEntity:数据库规范字段实体(主键、排序、创建/更新时间与操作人、备注、版本)
- BaseEntity:扩展基础实体(分页、查询、用户与租户编码等辅助字段),实现分页接口
Pageable - UserIdentity:用户身份实体(用户编码、用户名、昵称、头像、扩展属性)
2. 统一响应封装
- R<T>:统一 API 响应格式(响应码、消息、数据、请求/响应时间、耗时)
- PageData<T>:统一分页响应格式(分页信息 + 数据列表)
- CursorPageData<T>:游标分页响应格式(不统计总数,用于 C 端列表)
3. 异常处理体系
- CommonException:业务异常基类(持有
int code) - 7 个专用异常类:
ApiException、ApplicationException、NotFoundException、SystemException、UnauthorizedException、UserException、ValidationException - 异常工厂方法:支持字符串模板与结果码的静态工厂方法
of(...)
4. 结果码枚举 (ResultCode)
仅定义 HTTP 标准语义码(共 6 个):
- 200 SUCCESS、400 VALIDATION_ERROR、401 UNAUTHORIZED、403 FORBIDDEN、404 NOT_FOUND、500 ERROR
- 业务异常不使用预定义数字码,通过异常字符串消息描述(如
ValidationException.of("数据不存在"));需要数字码时使用CommonException.of(code, msg)自定义
5. 注解系统
- @Router:路由注解(类级别),声明模块与前缀
- @Schema(Swagger 注解):接口/字段描述,由 sh-core 直接引入依赖
6. 用户身份上下文管理
- IdentityContext:基于 ThreadLocal 的身份上下文工具类(全部静态方法,线程隔离)
- UserNameProvider:按用户编码批量获取姓名的 SPI 接口
7. 其他功能
- EnvType:系统环境类型枚举(DEV、SIT、UAT、PROD,含中文描述)
- MaskingPatternLayout:logback 日志脱敏 Layout
快速开始
1. 添加依赖
<dependency>
<groupId>com.wkclz.framework</groupId>
<artifactId>sh-core</artifactId>
</dependency>引入 sh-core 即自动传递依赖 sh-tool 与 swagger-annotations(@Schema)。
2. 使用基础实体
@Data
public class UserEntity extends BaseEntity {
@Schema(description = "用户名")
private String username;
@Schema(description = "邮箱")
private String email;
}DbColumnEntity 字段:id(主键)、sort(排序号,越大越往后)、createTime、createBy(创建人 code)、updateTime、updateBy(更新人 code)、remark、version(数据版本)。
BaseEntity 在 DbColumnEntity 之上扩展的字段:
| 分组 | 字段 | 说明 |
|---|---|---|
| 姓名回填 | createByName / updateByName | 创建人/更新人姓名 |
| 上下文 | userCode / tenantCode | 用户编码 / 租户编码 |
| 查询辅助 | orderBy / ids / keyword / timeFrom / timeTo | 排序规则 / 主键数组 / 模糊关键字 / 时间范围 |
| 分页辅助 | current / size / offset / total / count | 页码 / 大小 / 偏移 / 总数 / 统计数 |
| 调试 | debug | debug 模式参数 |
静态工具方法:
copy(source, target):全量属性复制;target为 null 时基于source类型自动创建copyIfNotNull(source, target):仅复制非 null 属性
3. 使用统一响应
R<T> 字段:code、msg、data、requestTime、responseTime、costTime。
静态工厂方法:
| 方法 | 说明 |
|---|---|
R.ok() / R.ok(data) | 成功(200) |
R.warn() / R.warn(msg) / R.warn(tpl, args) | 校验警告(400,消息支持模板) |
R.error() / R.error(msg) / R.error(tpl, args) | 内部错误(500) |
R.error(CommonException) | 按异常携带的 code/msg 返回 |
R.error(code, msg) | 自定义 code 与消息 |
@RestController
@Router(module = "user", prefix = "/api/user")
public class UserController {
@GetMapping("/{id}")
public R<UserEntity> getUser(@PathVariable Long id) {
UserEntity user = userService.getById(id);
if (user == null) {
throw NotFoundException.of("用户不存在,ID: {}", id);
}
return R.ok(user);
}
@PostMapping
public R<UserEntity> createUser(@RequestBody @Valid UserEntity user) {
UserEntity created = userService.create(user);
return R.ok(created);
}
}4. 使用分页查询
分页参数由 Pageable 接口提供,BaseEntity 已实现。调用 init() 后:current/size 为空或小于 1 时取默认值(DEFAULT_CURRENT=1、DEFAULT_SIZE=10),并自动计算 offset=(current-1)*size。
@Service
public class UserService {
public R<PageData<UserEntity>> getUsers(UserQuery query) {
query.init(); // 初始化分页参数:current / size / offset
List<UserEntity> userList = userMapper.selectUsers(query);
Long total = userMapper.countUsers(query);
return R.ok(PageData.fromEntity(query, userList));
}
}PageData<T> 字段:current、size、offset、total、count、records。
工厂方法:
PageData.of(records, total):默认页码 1、每页大小 10PageData.of(records, total, current, size):完整分页信息PageData.of(records, current, size):不传总数时自动以列表长度作为 totalPageData.empty()/PageData.empty(current, size):空分页结果PageData.fromEntity(entity, records):从 BaseEntity 复制分页信息PageData.convert(source, newRecords)/convert(source, newRecords, targetClass):保持分页信息转换数据类型pageData.convert(targetClass):实例方法,转换 records 元素类型(基于 BeanUtil)
5. 使用游标分页
CursorPageData<T> 字段:records(id 倒序)、hasMore(是否还有下一页)、nextCursor(下一页游标,无更多时为 null)。
CursorPageData.of(records, hasMore, nextCursor):快速创建(records 需调用方按 size 截断)CursorPageData.empty():空游标分页结果
public R<CursorPageData<UserEntity>> listByCursor(UserQuery query) {
// 游标分页不统计总数
List<UserEntity> records = userMapper.selectByCursor(query);
Long nextCursor = records.isEmpty() ? null : records.get(records.size() - 1).getId();
return R.ok(CursorPageData.of(records, 1, nextCursor));
}6. 使用异常处理
CommonException extends RuntimeException,持有 int code。7 个子类(ApiException、ApplicationException、NotFoundException、SystemException、UnauthorizedException、UserException、ValidationException)均提供 6 个构造器:(String)、(ResultCode)、(int, String)、(String, Throwable)、(ResultCode, Throwable)、(int, String, Throwable),以及 3 个静态工厂:of(String, Object...)、of(ResultCode)、of(int, String, Object...)。
@Service
public class OrderService {
public OrderEntity getOrder(Long orderId) {
OrderEntity order = orderMapper.selectById(orderId);
if (order == null) {
// 字符串消息,NotFoundException 默认 code=404
throw NotFoundException.of("订单不存在: {}", orderId);
}
if (order.getStatus() == OrderStatus.PAID) {
// 使用自定义消息
throw new ApplicationException("订单已支付,不能重复支付");
}
if (order.isExpired()) {
// 使用字符串模板
throw CommonException.of("订单 {} 已超时,请重新下单", orderId);
}
return order;
}
}7. 使用用户身份上下文
IdentityContext 为 ThreadLocal 实现、全部静态方法,线程隔离;不负责自动清理,由上层 Filter/Interceptor 在 finally 中调用 clear()。
| 方法 | 说明 |
|---|---|
set(UserIdentity, String token) | 设置当前线程身份与 Token(两者均不可为 null) |
get() | 获取 UserIdentity |
getToken() | 获取认证 Token |
getUserCode() / getUsername() / getNickname() / getAvatar() | 获取用户编码 / 用户名 / 昵称 / 头像 |
getAttribute(String key) | 获取扩展属性 |
setAppCode(String) / getAppCode() | 应用编码 |
setTenantCode(String) / getTenantCode() | 租户编码(未设置时返回 "default") |
clear() | 清理当前线程身份信息 |
UserIdentity 字段:userCode、username、nickname、avatar、attributes(扩展属性 Map),并提供 addAttribute(key, value) 便捷添加单个扩展属性。
// 认证过滤器:设置身份
UserIdentity identity = new UserIdentity();
identity.setUserCode("u10001");
identity.setUsername("zhangsan");
identity.addAttribute("openid", "wx-openid-xxx");
IdentityContext.set(identity, "token-xxx");// 业务代码:读取身份
String userCode = IdentityContext.getUserCode();
String tenantCode = IdentityContext.getTenantCode();
String openid = IdentityContext.getAttribute("openid");// 请求结束:清理(Filter finally 块)
IdentityContext.clear();8. 实现 SPI 扩展
UserNameProvider 接口提供默认实现 getNamesByUserCodes(Set<String>)(返回 Map<String,String>,默认空 Map)。需要按用户编码批量获取姓名时,实现并注册为 Spring Bean 即可。
@Component
public class CustomUserNameProvider implements UserNameProvider {
@Override
public Map<String, String> getNamesByUserCodes(Set<String> userCodes) {
if (userCodes == null || userCodes.isEmpty()) {
return Collections.emptyMap();
}
return userService.getByUserCodes(userCodes).stream()
.collect(Collectors.toMap(
UserEntity::getUserCode,
UserEntity::getUsername
));
}
}9. 使用日志脱敏
MaskingPatternLayout 继承 logback PatternLayout,通过 addMaskPattern(String) 注册正则脱敏规则,日志输出时将匹配的捕获组内容替换为 *。
<!-- logback.xml 配置 -->
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="com.wkclz.core.log.MaskingPatternLayout">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
<maskPattern>(\d{3})\d{4}(\d{4})</maskPattern> <!-- 手机号 -->
<maskPattern>(\w{3})\w+@(\w+\.\w+)</maskPattern> <!-- 邮箱 -->
<maskPattern>"password":"([^"]+)"</maskPattern> <!-- 密码 -->
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
</root>
</configuration>10. 系统环境类型
EnvType 枚举:DEV(开发环境)、SIT(集成测试环境)、UAT(验收测试环境)、PROD(生产环境),通过 getDesc() 获取中文描述。
最佳实践指南
1. 实体设计规范
- 统一继承 BaseEntity:所有数据库实体都应继承 BaseEntity
- 合理使用注解:为重要字段添加
@Schema(description = "...")描述 - 版本控制:使用
version字段实现乐观锁 - 分页参数初始化:查询前调用
query.init()初始化分页参数
2. 响应设计规范
- 统一响应格式:所有 API 返回
R<T>类型响应 - 合理使用状态码:HTTP 语义码由 ResultCode 提供,业务提示通过异常字符串消息描述
- 分页标准化:分页查询使用
PageData<T>封装,C 端游标列表使用CursorPageData<T> - 错误信息友好:提供清晰的错误提示信息
3. 异常处理规范
- 异常分类明确:根据异常类型选择合适异常类
- 异常信息友好:提供清晰的异常描述
- 异常链完整:保留原始异常信息
- 统一异常处理:使用
@ControllerAdvice统一处理,并通过R.error(CommonException)返回
4. 身份上下文规范
- 及时设置清除:在认证过滤器中设置,请求完成后(finally)清除
- 避免内存泄漏:确保 ThreadLocal 被正确清理
- 上下文验证:使用前判断身份是否存在(
get()返回 null 表示未登录) - SPI 扩展实现:根据需求实现 UserNameProvider
5. 日志安全规范
- 敏感信息脱敏:配置脱敏规则保护用户隐私
- 日志级别合理:根据重要性设置合适的日志级别
- 日志格式统一:使用统一的日志格式
- 异常日志完整:记录完整的异常堆栈信息
常见问题解答
Q1: 为什么需要继承 BaseEntity?
A: BaseEntity 提供了数据库通用字段、用户/租户上下文字段和分页查询辅助功能,避免在每个实体中重复定义这些字段,提高代码复用性和一致性。
Q2: 如何自定义异常消息格式?
A: 可以使用字符串模板功能:CommonException.of("用户 {} 不存在", userId),支持参数化消息。
Q3: 身份上下文在多线程环境下是否安全?
A: IdentityContext 基于 ThreadLocal 实现,每个线程有独立的副本,在多线程环境下是安全的。但需要注意在异步任务中手动传递身份上下文。
Q4: 如何扩展结果码体系?
A: ResultCode 仅维护 HTTP 标准语义码,业务提示不新增数字码,直接使用异常字符串消息(如 UserException.of("余额不足"));确需数字码时通过 CommonException.of(code, msg) 传递,避免与现有语义码冲突。
Q5: 日志脱敏支持哪些模式?
A: 支持正则表达式模式,可以配置手机号、邮箱、密码、身份证号等常见敏感信息的脱敏规则。
Q6: SPI 扩展如何生效?
A: 实现 UserNameProvider 接口并注册为 Spring Bean(如 @Component),即可在需要按用户编码批量获取姓名的场景中使用。
贡献指南
1. 代码规范
- 遵循 Java 编码规范
- 使用 Lombok 减少样板代码
- 添加必要的单元测试
- 保持向后兼容性
2. 文档要求
- 为新增功能添加使用示例
- 更新 API 文档
- 添加必要的注释
- 提供迁移指南(如有破坏性变更)
3. 测试要求
- 单元测试覆盖率不低于 80%
- 集成测试覆盖主要功能
- 性能测试验证关键路径
- 兼容性测试确保向后兼容
4. 发布流程
- 功能开发完成
- 编写测试用例
- 更新文档
- 代码审查
- 合并到主分支
- 版本发布
总结
Core 模块作为 sh-framework 的技术基础,提供了完整的基础技术能力支持。通过合理使用 Core 模块的各项功能,开发者可以:
- 大幅减少重复代码编写
- 统一技术实现标准
- 提高代码质量和可维护性
- 快速构建稳定可靠的应用程序
建议新项目从一开始就集成 Core 模块,充分利用其提供的标准化组件,确保项目技术架构的一致性和可维护性。