CC 咖啡猫的工作空间 Coding Space

工程实践

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();
    }
}

参考资料