一、工具类
- 三个工具类的定位与联动逻辑
| 工具类 | 核心定位 | 核心方法与作用 |
|---|---|---|
| MD5Utils | 加密工具(不可逆哈希加密) | ①md5(str):基础加密(非敏感数据);②md5(str, key):固定密钥加密(接口签名 / 临时数据);③md5Salt(str, salt):加盐加密(用户密码);④verifyOriginalAndCiphertext:密码验证(对比加密结果) |
| UUIDUtils | 唯一 ID 生成工具 | ①UUID_32():32 位无分隔符 UUID(盐值 / 数据库主键,紧凑省空间);②UUID_36():36 位标准 UUID(第三方对接 / 日志 ID,兼容性强) |
| StringUtils | 字符串通用处理工具 | 封装isEmpty()等方法(参数校验,避免重复代码) |
| 方法 | 核心场景 | 核心价值 |
|---|---|---|
| md5(String str) | 简单加密(非敏感数据) | 快速加密,无需额外参数 |
| md5(String str, key) | 固定密钥加密(接口签名、临时数据) | 统一密钥,便于系统级验证 |
| md5Salt(str, salt) | 用户密码加密 | 每个用户唯一密文,防批量破解 |
| UUID_32() | 盐值、数据库主键、短唯一 ID | 紧凑、无特殊字符,节省存储 |
| UUID_36() | 标准 UUID 场景、第三方对接 | 符合行业标准,兼容性强 |
| 方法 | 格式特点 | 适用场景 |
|---|---|---|
| UUID_32() | 32 位、无 - 分隔符 | 1. 数据库主键 / 用户 ID / 盐值(节省存储,32 位字符串比 36 位更紧凑) 2. 文件名 / 临时文件名(无特殊字符,避免解析问题) |
| UUID_36() | 36 位、带 - 分隔符 | 1. 符合 UUID 标准格式的场景(比如对接要求标准 UUID 的第三方系统) 2. 日志追踪 ID(可读性稍高,能快速识别是 UUID) |
联动逻辑(用户注册 / 登录核心流程):
- 注册:
StringUtils校验参数非空 →UUIDUtils.UUID_32()生成唯一盐值 →MD5Utils.md5Salt()加密密码 → 用户名 + 密码密文 + 盐值存入数据库; - 登录:
StringUtils校验参数 → 从数据库查询用户的盐值 + 密码密文 →MD5Utils.verifyOriginalAndCiphertext()用 "旧盐值" 加密输入密码,对比密文验证。
2. 密码加密与验证的核心逻辑(解决 "UUID 随机生成" 的核心疑问)
- 盐值(UUID)的生成时机:仅在用户注册时生成一次 ,与用户账号永久绑定并存入数据库,登录时仅读取不重新生成;
- 验证的本质:用 "注册时的旧盐值" 对登录输入的明文密码重新执行
md5Salt加密,对比加密结果与数据库中的密文是否一致,一致则验证通过; - UUID 的作用:不是 "每次变化",而是靠 "UUID 算法(时间戳 + MAC 地址 + 随机数)" 保证全局唯一性,让每个用户的盐值唯一,即使密码相同,加密后的密文也不同(防彩虹表破解)。
3. 盐值唯一性与 Session 的核心区别(解决 "唯一性靠 Session" 的疑问)
| 维度 | 盐值(UUID) | Session(会话) |
|---|---|---|
| 生成时机 | 注册时(永久) | 登录成功后(临时) |
| 存储位置 | 数据库(与用户绑定) | 服务器 / Redis(临时存储) |
| 核心作用 | 保证密码加密的唯一性 / 安全性 | 维护用户登录状态(在线标识) |
| 关联性 | 与 Session 无任何关联 | 仅在登录验证成功后生成 |
4. 工具类方法的场景适配逻辑(解决 "方法设计意义" 的疑问)
md5(str, key):非密码场景专用,靠 "固定密钥" 实现加密(如接口签名、临时 Token),即使 str 相同,key 不同则密文不同;UUID_32()/UUID_36():适配不同场景的唯一 ID 生成,前者紧凑省空间(盐值 / 主键),后者符合标准(第三方对接 / 日志),避免业务代码重复写格式处理逻辑;- 工具类设计本质:"一个方法解决一类场景",覆盖项目中所有加密 / 唯一 ID 生成需求,而非仅服务于 "密码加密" 单一场景。
总结
- 核心安全逻辑:用户密码的安全性靠 "唯一盐值 + MD5 加盐加密" 保障,盐值是注册时绑定用户的固定值,而非登录时动态生成;
- 工具类协作逻辑:UUIDUtils 提供唯一盐值,MD5Utils 负责加密 / 验证,StringUtils 做参数校验,三者构成 "参数校验→生成盐值→加密存库→验证登录" 的完整链路;
- 方法设计逻辑 :工具类中每个方法对应特定业务场景,如
md5(str, key)服务于固定密钥加密,UUID_32()服务于紧凑存储,避免 "一刀切" 的设计缺陷; - 概念边界:盐值的唯一性靠 UUID 算法本身保证,与 Session(登录状态)无关联,二者是完全独立的技术概念。
二、MyBatis 核心配置解析
1.核心
理解自动生成的 Mapper XML 中关键配置的关联关系、定义位置及复用方式,避免重复开发。
2.关键知识点
- 配置关联与独立关系
- 关联:Mapper 接口方法与 XML 中
<select>/<insert>标签通过id强绑定(如selectByUserName方法与同名 XML 标签); - 复用:
BaseResultMap(数据库字段与实体类映射规则)和Base_Column_List(表全数字段集合)是自动生成的可复用片段,通过resultMap和<include>标签引用; - 独立:不同 Mapper XML(如
UserMapper.xml与UserExtMapper.xml)是平级关系,需通过命名空间控制复用范围。
- 关联:Mapper 接口方法与 XML 中
- 核心标签 / 属性作用
parameterType:声明 SQL 入参类型(如java.lang.String、实体类全路径),直接定义在 SQL 标签上,无需额外查找;resultMap:定义查询结果与实体类的映射规则,需在同 XML 中查找同名<resultMap>标签(如BaseResultMap对应 User 实体映射);<include refid="Base_Column_List">:复用表全数字段,避免重复写字段名,定义在 XML 中的<sql>标签内。
- 复用与拓展方式
- 同命名空间:拓展 XML(如
UserExtMapper.xml)若与原 Mapper 同命名空间(com.example.forum.dao.UserMapper),可直接引用BaseResultMap和Base_Column_List; - 不同命名空间:需通过全路径引用(如
com.example.forum.dao.UserMapper.BaseResultMap); Base_Column_List灵活使用:不仅适用于全字段查询,也可作为自定义查询的字段参考(挑选用需字段),避免漏写字段。
- 同命名空间:拓展 XML(如
三、日志打印规范与实践
1.核心
明确 log.info() 的传参规则,确保日志输出有意义、无报错,同时优化日志写法。
2.关键知识点
- 传参规则
- 支持类型:字符串、基本数据类型、任意对象(框架自动调用
toString()转字符串); - 不建议传参:
null(输出无意义)、未初始化对象(抛空指针)、超大内容(日志冗余)、敏感信息(如密码)。
- 支持类型:字符串、基本数据类型、任意对象(框架自动调用
toString()方法的作用- 自动调用:日志占位符
{}会自动调用对象的toString(),无需手动调用(如枚举ResultCode.FAILED_USER_EXISTS可直接传入占位符); - 自定义必要性:若对象(实体类、枚举)未重写
toString(),会使用Object类默认实现(输出 "类名 @哈希值"),无业务价值;核心类需重写(如 User 类包含id/username等关键字段,枚举包含code/msg)。
- 自动调用:日志占位符
- 优化写法
- 用占位符
{}替代字符串拼接(性能更好、可读性更强),如log.info("{} username = {}", ResultCode.FAILED_USER_EXISTS, user.getUsername()); - 单个属性打印:直接通过
user.getUsername()获取属性(String / 数字等类型天然有意义toString()),无需关心实体类是否重写toString()。
- 用占位符
四、论坛项目核心优化总结(实体类、DTO、MyBatis、异常处理)
一、背景与诉求
在开发论坛用户 / 文章模块时,面临以下问题:
- 实体类返回前端时包含敏感 / 冗余字段(如时间、删除状态),需精准控制返回字段;
- MyBatis 自动生成的 XML 易覆盖自定义 SQL,需隔离自动生成与自定义 SQL;
- 接口参数接收不规范(普通参数),校验 / 文档需硬编码,效率低;
- 接口返回错误码不精准(默认返回 1000),需全局异常处理;
- 非必填字段(如头像)需在 Service 层设置默认值,同时支持用户后续修改。
二、分模块核心优化方案
模块 1:实体类优化(控制返回字段 + 日志可读性)
1. 控制前端返回字段(解决 "不想返回时间 / 删除状态" 问题)
- 核心思路:通过 Jackson 注解过滤字段,或用 DTO 解耦(推荐);
- 具体方案 :
- 临时方案(简单场景):给无需返回的字段加
@JsonIgnore(如deleteState/updateTime),序列化时直接忽略; - 规范方案(推荐):创建专属返回 DTO(如
ArticleDTO/UserDTO),只包含前端需要的字段,避免实体类耦合前端逻辑;
- 临时方案(简单场景):给无需返回的字段加
2. 增强日志可读性(补充 toString)
-
核心思路 :用 Lombok 的
@ToString替代手动重写,过滤敏感字段; -
具体方案:
// 实体类上添加,排除无需打印的字段
@ToString(exclude = {"deleteState", "salt", "password"})
@Getter
@Setter
@NoArgsConstructor
public class User implements Serializable { ... }
模块 2:MyBatis 配置优化(解决 "自定义 SQL 被覆盖" 问题)
1. 核心痛点
自动生成的UserMapper.xml会覆盖自定义 SQL,且子包 XML 无法被扫描。
2. 解决方案(物理隔离 + 配置适配)
-
步骤 1:隔离 XML 文件
- 自动生成的 XML:放在
resources/mapper根目录(如UserMapper.xml); - 自定义 SQL:放在
resources/mapper/extension子包(如UserExtMapper.xml); - 关键:自定义 XML 的
namespace必须与接口全路径一致(com.example.forum.dao.UserMapper),MyBatis 会自动合并同 namespace 的 SQL。
- 自动生成的 XML:放在
-
步骤 2:修改 MyBatis 配置
mybatis:
# 扫描mapper根目录+所有子包的XML
mapper-locations: classpath:mapper/**/*.xml
# 指向实体类包(而非DAO包),修正你之前的配置错误
type-aliases-package: com.example.forum.model
configuration:
map-underscore-to-camel-case: true # 开启下划线转驼峰 -
补充 :
@MapperScan("com.example.forum.dao")保证 DAO 接口被扫描,与 XML 扫描逻辑互补。
模块 3:接口参数优化(引入 DTO,解决 "参数校验 / 文档繁琐" 问题)
1. DTO 核心价值
- 定义:Data Transfer Object(数据传输对象),是 Controller 层专属的 "参数容器",与接口一一对应;
- 优势:一行注解搞定 "必填校验 + 接口文档示例",替代硬编码的
StringUtils.isEmpty。
2. 实操步骤(以用户注册为例)
-
步骤 1:新建 DTO 包与类
package com.example.forum.dto;
@Data // 自动生成getter/setter
@Schema(description = "用户注册请求参数")
public class UserRegisterDTO {
// 必填字段:加@NotBlank校验 + Swagger注解
@NotBlank(message = "用户名不能为空")
@Schema(description = "用户名", required = true, example = "zhangsan123")
private String username;// 非必填字段:不加校验注解,前端没传则为null @Schema(description = "头像地址(可选,不传用默认)", required = false) private String avatarUrl; // 其他字段...}
步骤 2:改造 Controller 接口
// 用@Valid触发校验,@RequestBody接收JSON参数
@PostMapping("/register")
public AppResult register(@Valid @RequestBody UserRegisterDTO dto) {
// 1. 手动校验业务逻辑(如密码一致性)
if (!dto.getPassword().equals(dto.getPasswordRepeat())) {
return AppResult.failed(ResultCode.FAILED_TWO_PWD_NOT_SAME);
}
// 2. DTO转实体类,非必填字段没传则为null
User user = new User();
user.setUsername(dto.getUsername());
user.setAvatarUrl(dto.getAvatarUrl());
// 3. 调用Service(默认值在Service层处理)
userService.createNormalUser(user);
return AppResult.success();
}
步骤 3:Service 层处理默认值
// Service层原有逻辑,无需修改
if (user.getAvatarUrl() == null || user.getAvatarUrl().trim().isEmpty()) {
// 非必填字段没传,设置默认值
user.setAvatarUrl("/static/images/default-avatar.png");
}
模块 4:全局异常处理(解决 "错误码返回不精准" 问题)
1. 核心痛点
未捕获的异常(如 MyBatis 绑定异常、参数校验异常)默认返回 1000(FAILED),无法精准定位问题。
2. 解决方案(全局异常处理器)
package com.example.forum.exception;
import com.example.forum.common.AppResult;
import com.example.forum.common.ResultCode;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.binding.BindingException;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice // 全局捕获Controller层异常
@Slf4j
public class GlobalExceptionHandler {
// 处理自定义业务异常(如用户已存在)
@ExceptionHandler(ApplicationException.class)
public AppResult handleApplicationException(ApplicationException errorResult) {
return AppResult.failed(errorResult);
}
// 处理DTO参数校验异常
@ExceptionHandler(BindException.class)
public AppResult handleBindException(BindException e) {
FieldError fieldError = e.getFieldError();
String msg = fieldError != null ? fieldError.getDefaultMessage() : "参数校验失败";
return AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE, msg);
}
// 处理MyBatis绑定异常(如SQL未找到)
@ExceptionHandler(BindingException.class)
public AppResult handleBindingException(BindingException e) {
log.error("MyBatis绑定异常: {}", e.getMessage());
return AppResult.failed(ResultCode.ERROR_SERVICES); // 返回服务器内部错误
}
// 兜底处理所有未捕获异常
@ExceptionHandler(Exception.class)
public AppResult handleException(Exception e) {
log.error("系统异常: {}", e.getMessage(), e);
return AppResult.failed(ResultCode.UNKNOWN);
}
}
模块 5:业务逻辑优化(头像默认值 + 修改接口)
1. 注册时默认头像
- 逻辑:Service 层判断
avatarUrl为 null / 空字符串时,设置统一默认地址(如/static/images/default-avatar.png); - 进阶:将默认地址配置在
application.yml中,通过@Value注入,避免硬编码。
2. 新增头像修改接口(RESTful 规范)
@PutMapping("/avatar")
@Operation(summary = "修改用户头像", description = "传入用户名和新头像地址")
public AppResult updateAvatar(
@RequestParam String username,
@RequestParam String avatarUrl) {
// 1. 校验参数
if (StringUtils.isEmpty(username) || StringUtils.isEmpty(avatarUrl)) {
return AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE);
}
// 2. 查询用户并更新
User user = userMapper.selectByUserName(username);
if (user == null) {
return AppResult.failed(ResultCode.FAILED_USER_NOT_EXISTS);
}
user.setAvatarUrl(avatarUrl.trim());
user.setUpdateTime(LocalDateTime.now());
userMapper.updateByPrimaryKeySelective(user);
return AppResult.success();
}
四、核心优化效果总结
- 代码规范:DTO 解耦前端参数与数据库实体,MyBatis 物理隔离自动生成 / 自定义 SQL,职责清晰;
- 效率提升:注解替代硬编码的参数校验 / 日志 toString / 接口文档,减少重复代码;
- 稳定性增强:全局异常处理器覆盖所有异常场景,错误码精准,便于定位问题;
- 扩展性更好:非必填字段默认值在 Service 层统一处理,新增字段只需修改 DTO,无需改动核心业务逻辑。
五、后续
- 所有接口统一使用 DTO 接收 / 返回参数(如登录接口创建
UserLoginDTO,文章列表返回ArticleListDTO); - 敏感字段(如密码、salt)在实体类中加
@JsonIgnore,且toString排除,避免泄露; - 默认值(如头像、性别)配置在
application.yml中,通过@Value注入,便于环境切换; - 接口文档(Swagger)自动生成后,测试前先核对参数必填 / 示例是否符合预期。
注意: 加 / 的路径 = 服务器根地址 + / 后面的内容比如:
/sign-up.html→http://127.0.0.1:58080/+sign-up.html=http://127.0.0.1:58080/sign-up.html/user/register→http://127.0.0.1:58080/+user/register=http://127.0.0.1:58080/user/register
| 路径写法 | 跳转 / 匹配的最终地址 | 适用场景 |
|---|---|---|
| /sign-up.html | 固定指向 http://127.0.0.1:58080/sign-up.html | 不管当前在哪个页面,都跳根目录的注册页 |
| sign-up.html | 随当前页面变化: ① 当前在 http://127.0.0.1:58080/ → 跳 http://127.0.0.1:58080/sign-up.html ② 当前在 http://127.0.0.1:58080/user/ → 跳 http://127.0.0.1:58080/user/sign-up.html | 仅适用于「同级目录」的跳转 |