工程实践
1. RESTful API 设计
1.1 基本原则
| 原则 |
说明 |
| 资源导向 |
用名词表示资源,不用动词 |
| HTTP 方法 |
GET/POST/PUT/PATCH/DELETE |
| 无状态 |
每个请求包含所有必要信息 |
| 分层系统 |
通过中间层解耦 |
1.2 URL 设计规范
✅ 正确示例:
GET /users # 获取用户列表
GET /users/123 # 获取单个用户
POST /users # 创建用户
PUT /users/123 # 更新用户(全量)
PATCH /users/123 # 更新用户(部分)
DELETE /users/123 # 删除用户
GET /users/123/orders # 获取用户的订单
❌ 错误示例:
GET /getUsers
POST /createUser
POST /user/delete
1.3 HTTP 状态码
| 状态码 |
含义 |
适用场景 |
| 200 |
OK |
成功获取/更新 |
| 201 |
Created |
成功创建 |
| 204 |
No Content |
成功删除 |
| 400 |
Bad Request |
参数错误 |
| 401 |
Unauthorized |
未认证 |
| 403 |
Forbidden |
无权限 |
| 404 |
Not Found |
资源不存在 |
| 409 |
Conflict |
资源冲突 |
| 500 |
Internal Error |
服务器错误 |
1.4 版本控制
/api/v1/users
/api/v2/users
1.5 分页设计
GET /users?page=1&size=20
GET /users?cursor=abc123&size=20 # 游标分页
响应格式:
{
"data": [...],
"pagination": {
"page": 1,
"size": 20,
"total": 1000,
"totalPages": 50
}
}
1.6 统一响应格式
{
"code": 200,
"message": "success",
"data": { },
"timestamp": "2024-01-15T10:30:00Z",
"requestId": "uuid-xxx"
}
错误响应:
{
"code": 400,
"message": "参数错误:用户名不能为空",
"errors": [
{"field": "username", "message": "不能为空"}
],
"timestamp": "2024-01-15T10:30:00Z"
}
2. 代码命名规范
2.1 基本原则
| 原则 |
说明 |
| 清晰 |
name → userName, orderId |
| 一致 |
统一风格,不要混用 |
| 简洁 |
避免冗余,但不要太短 |
| 可读 |
orderId 比 oi 更可读 |
| 有意义 |
用完整单词,不用缩写 |
2.2 Java 命名规范
| 类型 |
规范 |
示例 |
| 类名 |
UpperCamelCase |
UserService, OrderController |
| 方法名 |
lowerCamelCase |
getUserById, createOrder |
| 变量名 |
lowerCamelCase |
userName, orderList |
| 常量 |
UPPER_SNAKE_CASE |
MAX_RETRY_COUNT |
| 包名 |
全小写,点分隔 |
com.example.service |
2.3 数据库命名
| 类型 |
规范 |
示例 |
| 表名 |
小写下划线,复数 |
users, order_items |
| 字段名 |
小写下划线 |
user_name, create_time |
| 索引名 |
idx_表名_字段 |
idx_users_name |
| 外键 |
fk_表1_表2 |
fk_orders_users |
2.4 API 命名
GET /user-addresses # 资源用复数名词
POST /cart/items # 嵌套资源
GET /users/{id} # 路径参数
GET /users?status=active # 查询参数
2.5 常见缩写
tmp → temporary
msg → message
cfg/config → configuration
cnt → count
num → number
org → organization
dept → department
desc → description
env → environment
2.6 命名示例
// ✅ 好的命名
UserService userService;
List<Order> orders;
Map<String, User> userMap;
Set<String> activeUserIds;
// ❌ 不好的命名
UserService s; // 太短
List<Order> orderList; // List已经是列表
Map<String, User> map; // 太泛
3. 代码重构
3.1 重构时机
| 时机 |
说明 |
| 新功能前 |
降低后续开发成本 |
| 代码审查时 |
发现坏味道立即重构 |
| Bug修复时 |
借机改善代码质量 |
| 定期重构 |
小步快跑,持续优化 |
3.2 常见坏味道
| 坏味道 |
解决方案 |
| 重复代码 |
提取方法/类 |
| 过长方法 |
拆分为多个小方法 |
| 过大类 |
拆分为多个类 |
| 过长参数列表 |
封装为参数对象 |
| 发散式变化 |
提取抽象接口 |
| 霰弹式修改 |
合并相关代码到一处 |
3.3 重构手法
提取方法
// 重构前
public void createOrder(Order order) {
// 验证订单...
// 计算价格...
// 检查库存...
// 扣减库存...
// 保存订单...
// 发送通知...
}
// 重构后
public void createOrder(Order order) {
validateOrder(order);
calculatePrice(order);
deductInventory(order);
saveOrder(order);
sendNotification(order);
}
提取类
// 重构前:一个类承担太多职责
class UserService {
void saveUser() { }
void sendEmail() { }
void generateReport() { }
void backupData() { }
}
// 重构后:职责分离
class UserService { }
class EmailService { }
class ReportService { }
class BackupService { }
提取接口
// 重构前:直接依赖具体实现
class OrderService {
private MySQLRepository repo;
}
// 重构后:依赖抽象
class OrderService {
private OrderRepository repo; // 接口
}
3.4 重构检查清单
□ 代码是否可运行(保持测试通过)
□ 是否有单元测试覆盖
□ 是否引入了新的坏味道
□ 变更范围是否可控
□ 是否做好了版本控制
3.5 测试驱动重构
// 1. 先写测试
@Test
public void testCalculateDiscount() {
assertEquals(90, calculator.calculate(100, 0.1), 0.01);
}
// 2. 重构代码
public double calculate(double price, double rate) {
return price * (1 - rate);
}
// 3. 确保测试通过
4. 单元测试
4.1 单元测试原则 (FIRST)
| 原则 |
说明 |
| Fast |
测试执行快 |
| Independent |
测试间无依赖 |
| Repeatable |
可重复执行,结果一致 |
| Self-Validating |
结果自验证,不需要手动判断 |
| Timely |
与代码同步编写 |
4.2 测试结构 (AAA)
@Test
public void testTransferSuccess() {
// Arrange - 准备测试数据
Account from = new Account("A", 1000);
Account to = new Account("B", 500);
TransferService service = new TransferService();
// Act - 执行被测方法
service.transfer(from, to, 200);
// Assert - 验证结果
assertEquals(800, from.getBalance(), 0.01);
assertEquals(700, to.getBalance(), 0.01);
}
4.3 测试覆盖率
| 覆盖率 |
说明 |
| 行覆盖率 |
执行到的代码行数 |
| 分支覆盖率 |
if/else 分支是否覆盖 |
| 方法覆盖率 |
是否有测试覆盖每个方法 |
| 目标 |
核心业务 80%+,关键路径 100% |
4.4 常用框架
| 语言 |
框架 |
| Java |
JUnit5, Mockito |
| Python |
pytest, unittest |
| JavaScript |
Jest, Mocha |
| Go |
testing, testify |
4.5 JUnit5 示例
@Nested
@DisplayName("OrderService Tests")
class OrderServiceTest {
@Mock
private OrderRepository repository;
@InjectMocks
private OrderService service;
@Test
@DisplayName("创建订单成功")
void createOrderSuccess() {
Order order = new Order("P001", 2);
when(repository.save(any())).thenReturn(order);
Order result = service.create(order);
assertNotNull(result.getId());
verify(repository).save(order);
}
@Test
@DisplayName("库存不足时创建失败")
void createOrderWhenInventoryInsufficient() {
Order order = new Order("P001", 999999);
when(inventory.check("P001", 999999)).thenReturn(false);
assertThrows(InsufficientInventoryException.class,
() -> service.create(order));
}
@ParameterizedTest
@CsvSource({
"100, 0.9, 90",
"100, 0.8, 80",
"100, 1.0, 100"
})
@DisplayName("折扣计算正确")
void discountCalculation(double price, double rate, double expected) {
assertEquals(expected, calculator.calculate(price, rate), 0.01);
}
}
4.6 Mockito 示例
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository;
@Spy
private PasswordEncoder encoder = new BCryptEncoder();
@InjectMocks
private UserService userService;
@Test
void saveUserSuccess() {
User user = new User("test", "123456");
when(userRepository.findByName("test")).thenReturn(null);
when(userRepository.save(any(User.class))).thenAnswer(
i -> { User u = i.getArgument(0); u.setId(1L); return u; }
);
User result = userService.save(user);
assertEquals(1L, result.getId());
verify(encoder).encode("123456");
}
}
4.7 测试边界
@Test
void testEdgeCases() {
// 空值
assertThrows(IllegalArgumentException.class,
() -> service.process(null));
// 边界值
assertEquals(100, calculator.calculate(100, 0));
assertEquals(0, calculator.calculate(100, 1));
// 异常情况
assertThrows(NegativeAmountException.class,
() -> new Account(-100));
}
5. 认证与授权
5.1 认证 (Authentication)
验证"你是谁"。
| 方式 |
说明 |
| 用户名/密码 |
最常见 |
| 手机号/验证码 |
免密码登录 |
| OAuth |
第三方登录 |
| 生物识别 |
指纹、面部 |
| 证书/密钥 |
API 认证 |
5.2 授权 (Authorization)
决定"你能做什么"。
5.3 常见架构
认证服务 → 发放 Token → 后续请求携带 Token → 验证 Token → 获取权限
5.4 实现示例
// 认证接口
@PostMapping("/login")
public Result<LoginVO> login(@RequestBody @Valid LoginDTO dto) {
User user = userService.authenticate(dto.getUsername(), dto.getPassword());
if (user == null) {
return Result.fail("用户名或密码错误");
}
String token = jwtService.generateToken(user.getId());
return Result.success(new LoginVO(token, user.getId()));
}
// 鉴权注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequirePermission {
String[] value();
}
// 权限校验
@Aspect
@Component
public class PermissionAspect {
@Around("@annotation(requirePermission)")
public Object checkPermission(ProceedingJoinPoint join, RequirePermission ann) {
String token = getTokenFromRequest();
UserPermission perm = jwtService.getPermission(token);
for (String p : ann.value()) {
if (!perm.hasPermission(p)) {
throw new UnauthorizedException("无权限: " + p);
}
}
return join.proceed();
}
}
6. JWT 框架
6.1 JWT 结构
Header.Payload.Signature
eyJhbGciOiJIUzI1NiJ9.
eyJ1c2VySWQiOiIxMjM0In0.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
| 部分 |
内容 |
| Header |
算法类型 (HS256) |
| Payload |
用户信息、过期时间 |
| Signature |
签名,防止篡改 |
6.2 JWT 生成
public String generateToken(Long userId) {
Date now = new Date();
Date expiry = new Date(now.getTime() + 3600000); // 1小时
return Jwts.builder()
.setSubject(String.valueOf(userId))
.claim("role", "USER")
.setIssuedAt(now)
.setExpiration(expiry)
.signWith(Keys.hmacShaKeyFor(SECRET.getBytes()))
.compact();
}
6.3 JWT 验证
public Claims parseToken(String token) {
try {
return Jwts.parserBuilder()
.setSigningKey(Keys.hmacShaKeyFor(SECRET.getBytes()))
.build()
.parseClaimsJws(token)
.getBody();
} catch (JwtException e) {
throw new UnauthorizedException("Token无效");
}
}
6.4 Refresh Token
// Access Token: 短期 (15分钟-1小时)
// Refresh Token: 长期 (7天-30天)
@PostMapping("/refresh")
public Result<LoginVO> refresh(@RequestBody RefreshDTO dto) {
Claims claims = parseToken(dto.getRefreshToken());
if (isExpired(claims)) {
return Result.fail("Refresh Token已过期,请重新登录");
}
String newAccessToken = generateAccessToken(claims.getSubject());
return Result.success(new LoginVO(newAccessToken, null));
}
6.5 JJWT 依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
7. SSO 单点登录
7.1 SSO 原理
用户 → 应用A → 重定向到 SSO Server → 登录 → 返回 Token → 应用A
↓
用户 → 应用B → 重定向到 SSO Server → 已登录 → 返回 Token → 应用B
7.2 CAS 协议
| 流程 |
说明 |
| 1. 用户访问应用 |
应用检查无登录态 |
| 2. 重定向到 CAS |
携带 service 参数 |
| 3. CAS 登录页 |
用户输入账号密码 |
| 4. 验证成功 |
颁发 TGT(Ticket Granting Ticket) |
| 5. 重定向回应用 |
携带 ST(Service Ticket) |
| 6. 应用验证 ST |
验证通过后建立会话 |
| 7. 访问其他应用 |
携带 TGT 获取新 ST |
7.3 OAuth2.0 实现 SSO
// OAuth2 授权端点
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.oauth2Login()
.loginPage("/oauth/login")
.authorizationEndpoint()
.baseUri("/oauth/authorize")
.and()
.tokenEndpoint()
.accessTokenUri("https://sso.example.com/oauth/token")
.and()
.userInfoEndpoint()
.userService(new CustomOAuth2UserService());
}
}
// 登录成功后获取用户信息
@RestController
public class AuthController {
@GetMapping("/user")
public OAuth2AuthenticationToken user(OAuth2AuthenticationToken token) {
return token;
}
}
7.4 JWT 实现 SSO
SSO Server:
1. 验证登录
2. 生成 JWT(包含用户信息和签发者标识)
3. 返回 JWT
各应用:
1. 验证 JWT 签名(使用共享密钥或公钥)
2. 检查 iss 字段确认是本公司签发
3. 提取用户信息建立会话
7.5 CAS vs OAuth2 vs SAML
| 协议 |
场景 |
特点 |
| CAS |
自建 SSO |
简单,专用 |
| OAuth2 |
第三方登录 |
授权场景 |
| SAML |
企业集成 |
XML,复杂 |
8. 权限系统设计
8.1 权限模型
RBAC (Role-Based Access Control)
用户 → 角色 → 权限
↓
用户-角色(多对多)
角色-权限(多对多)
权限层级
| 层级 |
说明 |
示例 |
| 菜单权限 |
能否看到某个菜单 |
用户管理 |
| 按钮权限 |
能否点击某个按钮 |
新增、删除 |
| 数据权限 |
能看到哪些数据 |
本部门数据 |
| 字段权限 |
能看到哪些字段 |
脱敏处理 |
8.2 数据库设计
-- 用户表
CREATE TABLE user (
id BIGINT PRIMARY KEY,
username VARCHAR(50),
password VARCHAR(255),
status TINYINT
);
-- 角色表
CREATE TABLE role (
id BIGINT PRIMARY KEY,
code VARCHAR(50), -- 唯一标识
name VARCHAR(100),
description VARCHAR(255)
);
-- 权限表
CREATE TABLE permission (
id BIGINT PRIMARY KEY,
code VARCHAR(100), -- permission:user:create
name VARCHAR(100),
type VARCHAR(20), -- menu/button/data
parent_id BIGINT,
path VARCHAR(255),
sort INT
);
-- 用户-角色关联
CREATE TABLE user_role (
user_id BIGINT,
role_id BIGINT,
PRIMARY KEY (user_id, role_id)
);
-- 角色-权限关联
CREATE TABLE role_permission (
role_id BIGINT,
permission_id BIGINT,
PRIMARY KEY (role_id, permission_id)
);
8.3 代码实现
// 权限注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequirePermission {
String value(); // permission:user:create
}
// 权限校验
@Aspect
@Component
public class PermissionAspect {
@Autowired
private PermissionService permissionService;
@Around("@annotation(requirePermission)")
public Object check(ProceedingJoinPoint join, RequirePermission ann) {
Long userId = getCurrentUserId();
if (!permissionService.hasPermission(userId, ann.value())) {
throw new ForbiddenException("无此权限");
}
return join.proceed();
}
}
// 使用
@RestController
public class UserController {
@PostMapping("/users")
@RequirePermission("permission:user:create")
public Result create(@RequestBody UserDTO dto) {
// 创建用户
}
}
8.4 前端权限控制
// 路由守卫
router.beforeEach((to, from, next) => {
if (!hasPermission(to.meta.permission)) {
next('/403');
}
next();
});
// 按钮权限
<el-button v-if="hasPermission('user:delete')" @click="deleteUser">
删除
</el-button>
// 指令
app.directive('permission', {
inserted(el, binding) {
if (!hasPermission(binding.value)) {
el.remove();
}
}
});
8.5 数据权限
// 租户隔离
@Before("execution(* com.example.*.*(..))")
public void dataScopeFilter(JoinPoint join) {
Long userId = getCurrentUserId();
TenantContext.setTenantId(getTenantId(userId));
try {
join.proceed();
} finally {
TenantContext.clear();
}
}
// SQL 拦截
@Interceptor PLSQL
public String buildDataScopeSql(String sql) {
Long tenantId = TenantContext.getTenantId();
if (tenantId != null) {
return sql + " AND tenant_id = " + tenantId;
}
return sql;
}
9. 加密与签名
9.1 对称加密
| 算法 |
特点 |
场景 |
| AES |
安全,快 |
数据加密 |
| DES |
不安全,已淘汰 |
老系统 |
| 3DES |
过渡方案 |
兼容老系统 |
// AES 加密
public String encrypt(String data, String key) {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
SecretKeySpec spec = new SecretKeySpec(key.getBytes(), "AES");
cipher.init(Cipher.ENCRYPT_MODE, spec, new IvParameterSpec(iv));
byte[] encrypted = cipher.doFinal(data.getBytes());
return Base64.encodeBase64String(encrypted);
}
9.2 非对称加密
| 算法 |
特点 |
场景 |
| RSA |
公钥加密,私钥解密 |
密钥交换 |
| ECC |
同等安全,更短密钥 |
移动设备 |
// RSA 加密
public String encrypt(String data, PublicKey publicKey) {
Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
byte[] encrypted = cipher.doFinal(data.getBytes());
return Base64.encodeBase64String(encrypted);
}
9.3 哈希算法
| 算法 |
输出 |
用途 |
| MD5 |
128位 |
已不安全 |
| SHA-1 |
160位 |
已不安全 |
| SHA-256 |
256位 |
密码存储、完整性校验 |
| SHA-512 |
512位 |
高安全场景 |
// 密码存储
public String hashPassword(String password) {
return new BCryptPasswordEncoder().encode(password);
}
public boolean verifyPassword(String password, String hash) {
return new BCryptPasswordEncoder().matches(password, hash);
}
9.4 数字签名
发送方:
1. 对消息计算摘要
2. 用私钥签名摘要
3. 发送消息 + 签名
接收方:
1. 用公钥验证签名
2. 对消息计算摘要
3. 比较摘要,一致则验证通过
// RSA 签名
public String sign(String data, PrivateKey privateKey) {
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initSign(privateKey);
signature.update(data.getBytes());
return Base64.encodeBase64String(signature.sign());
}
public boolean verify(String data, String signatureStr, PublicKey publicKey) {
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initVerify(publicKey);
signature.update(data.getBytes());
return signature.verify(Base64.decodeBase64(signatureStr));
}
9.5 常见场景
| 场景 |
方案 |
| 密码存储 |
BCrypt |
| 数据加密 |
AES + 密钥管理服务 |
| 密钥交换 |
RSA 或 DH |
| 消息完整性 |
HMAC-SHA256 |
| 数字签名 |
RSA + SHA256 |
10. 敏感词过滤
10.1 方案对比
| 方案 |
速度 |
内存 |
实现 |
| DFA |
快 |
中 |
确定有限自动机 |
| AC自动机 |
快 |
高 |
多模式匹配 |
| Trie树 |
快 |
中 |
前缀树 |
| 数据库 LIKE |
慢 |
低 |
不推荐 |
10.2 DFA 算法
public class SensitiveWordFilter {
private Map<String, Object> dict = new HashMap<>();
public void buildDict(List<String> words) {
for (String word : words) {
Map<String, Object> current = dict;
for (char c : word.toCharArray()) {
Map<String, Object> next = (Map<String, Object>) current.get(c);
if (next == null) {
next = new HashMap<>();
current.put(c, next);
}
current = next;
}
current.put("isEnd", true); // 标记结束
}
}
public String filter(String text) {
StringBuilder result = new StringBuilder();
for (int i = 0; i < text.length(); i++) {
int len = check(text, i);
if (len > 0) {
result.append("*".repeat(len));
i += len - 1;
} else {
result.append(text.charAt(i));
}
}
return result.toString();
}
private int check(String text, int start) {
Map<String, Object> current = dict;
int len = 0;
for (int i = start; i < text.length(); i++) {
Object next = current.get(text.charAt(i));
if (next == null) break;
current = (Map<String, Object>) next;
len++;
if (current.containsKey("isEnd")) return len;
}
return 0;
}
}
10.3 使用示例
@Configuration
public class SensitiveWordConfig {
@Bean
public SensitiveWordFilter sensitiveWordFilter() {
List<String> words = Arrays.asList("敏感词1", "敏感词2");
SensitiveWordFilter filter = new SensitiveWordFilter();
filter.buildDict(words);
return filter;
}
}
// 使用
@Service
public class ContentService {
@Autowired
private SensitiveWordFilter filter;
public String publish(String content) {
String filtered = filter.filter(content);
// 保存过滤后的内容
}
}
11. 数据脱敏
11.1 常见脱敏规则
| 类型 |
原始值 |
脱敏后 |
方法 |
| 手机号 |
13812345678 |
138****5678 |
中间四位 |
| 身份证 |
110101199001011234 |
110101********1234 |
显示头尾 |
| 姓名 |
张三 |
张* |
显示姓 |
| 邮箱 |
user@example.com |
u***@example.com |
保留首尾 |
| 银行卡 |
6222021234567890 |
622202********7890 |
显示头尾 |
| 地址 |
北京市朝阳区XX路 |
北京市朝阳区 |
部分隐藏 |
11.2 实现示例
public class DataMaskingUtil {
public static String maskPhone(String phone) {
if (phone == null || phone.length() != 11) return phone;
return phone.substring(0, 3) + "****" + phone.substring(7);
}
public static String maskIdCard(String idCard) {
if (idCard == null || idCard.length() < 8) return idCard;
return idCard.substring(0, 6) + "********" + idCard.substring(idCard.length() - 4);
}
public static String maskName(String name) {
if (name == null || name.isEmpty()) return name;
if (name.length() == 1) return name;
if (name.length() == 2) return name.charAt(0) + "*";
return name.charAt(0) + "*".repeat(name.length() - 1);
}
public static String maskEmail(String email) {
if (email == null || !email.contains("@")) return email;
String[] parts = email.split("@");
String prefix = parts[0];
if (prefix.length() <= 2) {
return prefix.charAt(0) + "***@" + parts[1];
}
return prefix.charAt(0) + "***" + prefix.charAt(prefix.length() - 1) + "@" + parts[1];
}
}
11.3 注解方式
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Masked {
MaskType type() default MaskType.PHONE;
}
public enum MaskType {
PHONE, ID_CARD, NAME, EMAIL, BANK_CARD, CUSTOM
}
// 使用
@Data
public class UserVO {
@Masked(type = MaskType.PHONE)
private String phone;
@Masked(type = MaskType.NAME)
private String name;
}
// 切面处理
@Aspect
@Component
public class MaskingAspect {
@Around("execution(* com.example.*.*Mapper.*(..))")
public Object around(ProceedingJoinPoint point) throws Throwable {
Object result = point.proceed();
// 反射处理 @Masked 注解
maskFields(result);
return result;
}
}
12. 数据校验
12.1 分层校验
Controller 层:格式校验(@Valid + BindingResult)
Service 层:业务规则校验
DAO 层:数据类型校验
12.2 常用注解
| 注解 |
说明 |
| @NotNull |
不能为 null |
| @NotEmpty |
不能为 null 或空字符串 |
| @NotBlank |
不能为 null/空串/纯空格 |
| @Size |
长度范围 |
| @Min/@Max |
数值范围 |
| @Email |
邮箱格式 |
| @Pattern |
正则表达式 |
| @Length |
字符串长度(Hibernate) |
| @Range |
范围(Hibernate) |
12.3 分组校验
// 定义分组
public interface Create {}
public interface Update {}
// 实体使用分组
@Data
public class User {
@NotNull(groups = {Create.class})
private Long id; // Create 时不能为空,Update 时可以为空
@NotBlank(groups = {Create.class, Update.class})
private String name;
}
// Controller 指定分组
@PostMapping
public Result create(@Validated(Create.class) @RequestBody User user) {
// create logic
}
@PutMapping
public Result update(@Validated(Update.class) @RequestBody User user) {
// update logic
}
12.4 自定义校验
// 1. 定义校验注解
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneValidator.class)
public @interface Phone {
String message() default "手机号格式错误";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
// 2. 实现校验器
public class PhoneValidator implements ConstraintValidator<Phone, String> {
private static final Pattern PHONE_PATTERN = Pattern.compile("^1[3-9]\\d{9}$");
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
return value != null && PHONE_PATTERN.matcher(value).matches();
}
}
// 3. 使用
@Data
public class UserDTO {
@Phone
private String phone;
}
12.5 全局异常处理
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result handleValidation(MethodArgumentNotValidException e) {
List<String> errors = e.getBindingResult()
.getFieldErrors()
.stream()
.map(FieldError::getDefaultMessage)
.collect(Collectors.toList());
return Result.fail(400, "参数错误", errors);
}
@ExceptionHandler(ConstraintViolationException.class)
public Result handleConstraintViolation(ConstraintViolationException e) {
List<String> errors = e.getConstraintViolations()
.stream()
.map(ConstraintViolation::getMessage)
.collect(Collectors.toList());
return Result.fail(400, "参数错误", errors);
}
}
13. 定时任务
13.1 实现方式对比
| 方案 |
优点 |
缺点 |
适用场景 |
| Thread.sleep |
简单 |
阻塞进程,不可靠 |
测试 |
| Timer |
简单 |
单线程,异常丢失 |
简单任务 |
| ScheduledExecutor |
多线程,异常可控 |
无分布式 |
中小型任务 |
| Quartz |
分布式,持久化,复杂 |
配置复杂 |
企业级 |
| XXL-JOB |
分布式,界面完善 |
需要额外部署 |
分布式任务 |
13.2 Spring @Scheduled
@Configuration
@EnableScheduling
public class SchedulingConfig {
@Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点
public void dailyTask() {
// 每日任务
}
@Scheduled(fixedDelay = 5000) // 上次执行后5秒
public void fixedDelayTask() {
// 固定延迟
}
@Scheduled(fixedRate = 10000) // 每10秒(无论上次是否完成)
public void fixedRateTask() {
// 固定频率
}
}
13.3 Quartz 示例
// 1. 定义 Job
public class OrderCleanupJob implements Job {
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
// 清理过期订单
}
}
// 2. 配置 Trigger
@Bean
public Trigger orderCleanupTrigger() {
CronScheduleBuilder schedule = CronScheduleBuilder
.cronSchedule("0 0 3 * * ?"); // 每天凌晨3点
return TriggerBuilder.newTrigger()
.forJob(orderCleanupJobDetail())
.withSchedule(schedule)
.build();
}
// 3. 集群配置
@Bean
public SchedulerFactoryBean schedulerFactoryBean() {
SchedulerFactoryBean factory = new SchedulerFactoryBean();
factory.setSchedulerName("clusterScheduler");
factory.setAutoStartup(true);
factory.setQuartzProperties(quartzProperties());
return factory;
}
13.4 XXL-JOB 使用
// 1. 开发 Handler
@XxlJob("orderCleanupJob")
public ReturnT<String> execute(String param) {
// 清理逻辑
return ReturnT.SUCCESS;
}
// 2. 注册执行器
@Bean
public XxlJobSpringExecutor xxlJobExecutor() {
XxlJobSpringExecutor executor = new XxlJobSpringExecutor();
executor.setXxlJobAdminAddresses("http://xxl-job:8080/xxl-job-admin");
executor.setAppname("order-service");
executor.setPort(9999);
executor.setAccessToken("xxl-job-token");
return executor;
}
// 3. 界面配置任务
// - 路由策略:第一个、最后一个、轮询、随机
// - 阻塞策略:单机串行、阻塞丢弃、覆盖
// - 失败重试:失败后重试次数
13.5 分布式任务注意事项
| 问题 |
解决方案 |
| 重复执行 |
分布式锁(Redisson) |
| 任务丢失 |
持久化 + 恢复 |
| 任务依赖 |
任务编排(DAG) |
| 监控告警 |
任务失败告警 |
@Scheduled(cron = "0 0 2 * * ?")
public void dailyReport() {
// 获取分布式锁
RLock lock = redissonClient.getLock("dailyReport");
try {
if (lock.tryLock(0, 1, TimeUnit.HOURS)) {
// 执行任务
generateReport();
}
} finally {
lock.unlock();
}
}
参考资料