实战一:用户 REST 服务(分层 + 校验 + JWT + 日志)
把前面章节的能力串成一个可运行骨架:分层架构做用户 REST 服务,入参校验放 Controller,事务与日志在 Service,写接口用 JWT + 方法级权限保护。示例基于 Boot 3.x(Spring Data JPA + H2,教学精简版)。
分层目录
src/main/java/com/example/user/
├── UserApplication.java
├── controller/UserController.java # 收参数、出响应
├── service/UserService.java # 业务与事务
├── repository/UserRepository.java
├── entity/User.java
├── dto/{UserCreateRequest,UserDto}.java
└── exception/{BizException,GlobalExceptionHandler}.java
实体与仓储
实体与接口分属两个文件,此处合并展示:
@Entity
public class User {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String username;
private String password; // 生产按第 30 章存 BCrypt 密文
private String email;
// getter/setter 省略
}
public interface UserRepository extends JpaRepository<User, Long> {
boolean existsByUsername(String username);
}
入参校验(jakarta validation)
public record UserCreateRequest(
@NotBlank(message = "用户名不能为空")
@Size(min = 2, max = 20, message = "用户名长度 2~20") String username,
@NotBlank(message = "密码不能为空")
@Size(min = 6, message = "密码至少 6 位") String password,
@Email(message = "邮箱格式不正确") String email) {}
Service:事务 + 日志
@Service
public class UserService {
private static final Logger log = LoggerFactory.getLogger(UserService.class);
private final UserRepository userRepository;
@Transactional(rollbackFor = Exception.class)
public UserDto create(UserCreateRequest req) {
if (userRepository.existsByUsername(req.username())) {
throw new BizException(ErrorCode.BIZ_ERROR, "用户名已存在");
}
User saved = userRepository.save(new User(req.username(), req.password(), req.email()));
log.info("创建用户 id={}, username={}", saved.getId(), saved.getUsername());
// 输出:创建用户 id=1, username=zhangsan
return UserDto.from(saved);
}
}
Controller:JWT + 方法级权限
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
@PostMapping
@PreAuthorize("hasRole('ADMIN')") // 需 @EnableMethodSecurity(第 32 章)
public UserDto create(@Valid @RequestBody UserCreateRequest req) {
return userService.create(req);
}
@GetMapping("/{id}")
public UserDto get(@PathVariable Long id) {
return userService.findById(id); // 不存在抛 BizException(404,...)
}
}
安全链按第 31 章配置:JWT 验签通过才进业务,写接口要 ADMIN,读接口登录即可;401/403 输出见第 33 章的统一 JSON。
全局异常处理
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, Object>> onInvalid(MethodArgumentNotValidException e) {
String msg = e.getBindingResult().getFieldErrors().stream()
.map(f -> f.getField() + ": " + f.getDefaultMessage())
.collect(Collectors.joining("; "));
// 输出:400 + {"code":400,"message":"password: 密码至少 6 位"}
return ResponseEntity.badRequest().body(Map.of("code", 400, "message", msg));
}
@ExceptionHandler(BizException.class)
public ResponseEntity<Map<String, Object>> onBiz(BizException e) {
return ResponseEntity.status(e.getHttpStatus())
.body(Map.of("code", e.getCode(), "message", e.getMessage()));
}
}
验证与日志要点
- 日志用占位符
{}传参,异常打全堆栈log.error("...", e); - 链路日志带 traceId(第 40 章),排障全靠它;
- 补测试(第 24/25 章):创建成功、用户名重复、无 JWT 401、无权限 403。 小结:本章骨架 = Controller(校验) → Service(事务+日志) → Repository,外层套 JWT 认证与 @PreAuthorize 授权,全局异常兜底;统一返回体在下一章升级为 Result。