重构Controller的黄金法则:让你的代码优雅如诗!
重构Controller的黄金法则:让你的代码优雅如诗!
作为一名资深后端开发,你有没有遇到过这样的场景:接手一个老项目,打开Controller文件,密密麻麻的代码让人眼花缭乱,业务逻辑和控制逻辑混在一起,异常处理到处都是try-catch,返回格式五花八门...
今天就来聊聊如何重构Controller,让你的代码优雅如诗,告别那些让人头疼的"意大利面条式"代码!
一、Controller的职责定位:守住边界,各司其职
在开始重构之前,我们先要明确Controller的职责边界。一个好的Controller应该像一个优秀的项目经理,只负责协调和调度,而不应该亲自下场搬砖。
1.1 Controller应该做什么
- 接收请求:解析HTTP请求参数
- 参数校验:验证请求参数的合法性
- 调用服务:将请求转发给相应的Service层
- 返回响应:将处理结果封装成统一格式返回
1.2 Controller不应该做什么
- 业务逻辑:复杂的业务计算、数据处理
- 数据持久化:直接操作数据库
- 异常处理:到处写try-catch
- 工具方法:字符串处理、日期转换等
记住一句话:Controller只负责"接客",具体的"干活"交给Service层!
二、黄金法则一:单一职责原则
单一职责原则(SRP)是重构Controller的第一法则。每个Controller应该只负责一个业务领域或功能模块。
2.1 按业务模块划分
// ❌ 不好的做法:一个Controller处理所有业务
@RestController
@RequestMapping("/api")
public class GodController {
// 用户相关接口
@PostMapping("/user/login")
public Result login() { /* ... */ }
@PostMapping("/user/register")
public Result register() { /* ... */ }
// 订单相关接口
@PostMapping("/order/create")
public Result createOrder() { /* ... */ }
@GetMapping("/order/list")
public Result getOrderList() { /* ... */ }
// 商品相关接口
@GetMapping("/product/detail")
public Result getProductDetail() { /* ... */ }
}
// ✅ 好的做法:按模块拆分
@RestController
@RequestMapping("/api/user")
public class UserController {
@PostMapping("/login")
public Result login() { /* ... */ }
@PostMapping("/register")
public Result register() { /* ... */ }
}
@RestController
@RequestMapping("/api/order")
public class OrderController {
@PostMapping("/create")
public Result createOrder() { /* ... */ }
@GetMapping("/list")
public Result getOrderList() { /* ... */ }
}
@RestController
@RequestMapping("/api/product")
public class ProductController {
@GetMapping("/detail")
public Result getProductDetail() { /* ... */ }
}
2.2 按功能职责划分
即使是同一个业务模块,也可以根据功能进一步拆分:
// 用户模块可以拆分为多个Controller
@RestController
@RequestMapping("/api/user")
public class UserAuthController {
// 认证相关接口
}
@RestController
@RequestMapping("/api/user")
public class UserProfileController {
// 用户资料相关接口
}
@RestController
@RequestMapping("/api/user")
public class UserAddressController {
// 用户地址相关接口
}
三、黄金法则二:统一响应格式
一个优雅的API应该有统一的响应格式,这样前端开发才能愉快地对接。
3.1 定义统一响应类
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Result<T> {
/**
* 响应码
*/
private Integer code;
/**
* 响应消息
*/
private String message;
/**
* 响应数据
*/
private T data;
/**
* 时间戳
*/
private Long timestamp;
/**
* 成功响应
*/
public static <T> Result<T> success(T data) {
return Result.<T>builder()
.code(200)
.message("操作成功")
.data(data)
.timestamp(System.currentTimeMillis())
.build();
}
/**
* 失败响应
*/
public static <T> Result<T> error(Integer code, String message) {
return Result.<T>builder()
.code(code)
.message(message)
.timestamp(System.currentTimeMillis())
.build();
}
/**
* 失败响应(使用默认错误码)
*/
public static <T> Result<T> error(String message) {
return error(500, message);
}
}
3.2 使用ResponseBodyAdvice自动封装
为了让Controller更简洁,我们可以使用ResponseBodyAdvice来自动封装响应:
@RestControllerAdvice
public class ResponseWrapperAdvice implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter returnType,
Class<? extends HttpMessageConverter<?>> converterType) {
// 判断是否需要包装,可以根据注解或返回类型来判断
return !returnType.getDeclaringClass().isAnnotationPresent(IgnoreResponseWrapper.class)
&& !returnType.hasMethodAnnotation(IgnoreResponseWrapper.class);
}
@Override
public Object beforeBodyWrite(Object body,
MethodParameter returnType,
MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request,
ServerHttpResponse response) {
// 如果已经是Result类型,直接返回
if (body instanceof Result) {
return body;
}
// 如果是字符串类型,需要特殊处理
if (body instanceof String) {
try {
ObjectMapper objectMapper = new ObjectMapper();
return objectMapper.writeValueAsString(Result.success(body));
} catch (Exception e) {
return Result.success(body);
}
}
// 其他情况统一包装
return Result.success(body);
}
}
// 忽略响应包装的注解
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface IgnoreResponseWrapper {
}
3.3 Controller中的应用
@RestController
@RequestMapping("/api/user")
public class UserController {
@Autowired
private UserService userService;
// 使用统一响应格式
@PostMapping("/login")
public Result<UserInfoVO> login(@RequestBody LoginRequest request) {
UserInfoVO userInfo = userService.login(request);
return Result.success(userInfo);
}
// 不需要手动包装,由ResponseBodyAdvice自动处理
@GetMapping("/profile")
public UserInfoVO getProfile() {
return userService.getProfile();
}
// 忽略响应包装的情况
@IgnoreResponseWrapper
@GetMapping("/health")
public String health() {
return "OK";
}
}
四、黄金法则三:优雅的参数校验
参数校验是保证系统稳定性的第一道防线,应该做到优雅而不冗余。
4.1 使用JSR-303注解校验
@Data
public class UserRegisterRequest {
@NotBlank(message = "用户名不能为空")
@Length(min = 4, max = 20, message = "用户名长度必须在4-20个字符之间")
private String username;
@NotBlank(message = "密码不能为空")
@Pattern(regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)[a-zA-Z\\d@$!%*?&]{8,}$",
message = "密码必须包含大小写字母和数字,长度至少8位")
private String password;
@NotBlank(message = "手机号不能为空")
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
private String phone;
@Email(message = "邮箱格式不正确")
private String email;
@Min(value = 1, message = "年龄必须大于0")
@Max(value = 150, message = "年龄不能超过150")
private Integer age;
}
4.2 分组校验
// 创建分组接口
public interface CreateGroup {}
public interface UpdateGroup {}
@Data
public class UserRequest {
@Null(groups = CreateGroup.class, message = "创建时ID必须为空")
@NotNull(groups = UpdateGroup.class, message = "更新时ID不能为空")
private Long id;
@NotBlank(groups = {CreateGroup.class, UpdateGroup.class}, message = "用户名不能为空")
private String username;
@NotBlank(groups = CreateGroup.class, message = "密码不能为空")
private String password;
}
@RestController
@RequestMapping("/api/user")
public class UserController {
@PostMapping
public Result<String> createUser(@Validated(CreateGroup.class) @RequestBody UserRequest request) {
// 创建用户逻辑
return Result.success("创建成功");
}
@PutMapping
public Result<String> updateUser(@Validated(UpdateGroup.class) @RequestBody UserRequest request) {
// 更新用户逻辑
return Result.success("更新成功");
}
}
4.3 自定义校验注解
// 自定义手机号校验注解
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = MobileValidator.class)
@Documented
public @interface Mobile {
String message() default "手机号格式不正确";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
// 校验器实现
public class MobileValidator implements ConstraintValidator<Mobile, String> {
private static final String MOBILE_PATTERN = "^1[3-9]\\d{9}$";
@Override
public boolean isValid(String mobile, ConstraintValidatorContext context) {
if (mobile == null || mobile.isEmpty()) {
return true; // 空值校验交给@NotBlank处理
}
return mobile.matches(MOBILE_PATTERN);
}
}
五、黄金法则四:全局异常处理
优雅的异常处理能让系统更加健壮,用户体验更好。
5.1 定义统一异常类
@Data
@AllArgsConstructor
public class BizException extends RuntimeException {
private Integer code;
private String message;
public BizException(String message) {
this.code = 500;
this.message = message;
}
public BizException(Integer code, String message) {
this.code = code;
this.message = message;
}
}
// 业务异常枚举
@Getter
@AllArgsConstructor
public enum BizErrorCode {
USER_NOT_FOUND(1001, "用户不存在"),
USER_ALREADY_EXISTS(1002, "用户已存在"),
PARAM_ERROR(1003, "参数错误"),
PERMISSION_DENIED(1004, "权限不足");
private final Integer code;
private final String message;
}
5.2 全局异常处理器
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* 处理自定义业务异常
*/
@ExceptionHandler(BizException.class)
public Result<Void> handleBizException(BizException e) {
log.warn("业务异常: {}", e.getMessage());
return Result.error(e.getCode(), e.getMessage());
}
/**
* 处理参数校验异常
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<Void> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
StringBuilder errorMsg = new StringBuilder();
e.getBindingResult().getFieldErrors().forEach(error ->
errorMsg.append(error.getField()).append(error.getDefaultMessage()).append(";")
);
log.warn("参数校验异常: {}", errorMsg.toString());
return Result.error(400, errorMsg.toString());
}
/**
* 处理ConstraintViolationException异常
*/
@ExceptionHandler(ConstraintViolationException.class)
public Result<Void> handleConstraintViolationException(ConstraintViolationException e) {
StringBuilder errorMsg = new StringBuilder();
e.getConstraintViolations().forEach(violation ->
errorMsg.append(violation.getMessage()).append(";")
);
log.warn("参数校验异常: {}", errorMsg.toString());
return Result.error(400, errorMsg.toString());
}
/**
* 处理HTTP请求方法不支持异常
*/
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public Result<Void> handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) {
log.error("请求方法不支持: {}", e.getMessage());
return Result.error(405, "请求方法不支持");
}
/**
* 处理系统异常
*/
@ExceptionHandler(Exception.class)
public Result<Void> handleException(Exception e) {
log.error("系统异常: ", e);
return Result.error(500, "系统异常,请联系管理员");
}
}
六、黄金法则五:合理使用注解和配置
SpringBoot提供了丰富的注解和配置选项,合理使用能让代码更加简洁优雅。
6.1 简化Controller配置
@RestController
@RequestMapping("/api/user")
@Validated // 启用方法级别参数校验
@Api(tags = "用户管理") // Swagger文档注解
public class UserController {
@Autowired
private UserService userService;
@PostMapping("/login")
@ApiOperation("用户登录")
@ApiImplicitParams({
@ApiImplicitParam(name = "username", value = "用户名", required = true, dataType = "string"),
@ApiImplicitParam(name = "password", value = "密码", required = true, dataType = "string")
})
public Result<UserInfoVO> login(@RequestBody @Valid LoginRequest request) {
UserInfoVO userInfo = userService.login(request);
return Result.success(userInfo);
}
@GetMapping("/{id}")
@ApiOperation("获取用户信息")
@ApiImplicitParam(name = "id", value = "用户ID", required = true, dataType = "long")
public Result<UserInfoVO> getUserById(@PathVariable @Min(1) Long id) {
UserInfoVO userInfo = userService.getUserById(id);
return Result.success(userInfo);
}
}
6.2 使用构造器注入
@RestController
@RequestMapping("/api/user")
public class UserController {
private final UserService userService;
private final RedisTemplate<String, Object> redisTemplate;
// 构造器注入,推荐使用
public UserController(UserService userService,
RedisTemplate<String, Object> redisTemplate) {
this.userService = userService;
this.redisTemplate = redisTemplate;
}
}
七、实战案例:重构前后的对比
让我们通过一个实际案例来看看重构前后的差异:
7.1 重构前的代码
@RestController
public class BadUserController {
@Autowired
private UserService userService;
@Autowired
private OrderService orderService;
@Autowired
private RedisTemplate redisTemplate;
@PostMapping("/user/login")
public Map<String, Object> login(HttpServletRequest request) {
Map<String, Object> result = new HashMap<>();
try {
String username = request.getParameter("username");
String password = request.getParameter("password");
// 业务逻辑混在Controller中
if (username == null || username.trim().isEmpty()) {
result.put("code", 400);
result.put("msg", "用户名不能为空");
return result;
}
if (password == null || password.trim().isEmpty()) {
result.put("code", 400);
result.put("msg", "密码不能为空");
return result;
}
// 调用服务
User user = userService.login(username, password);
if (user == null) {
result.put("code", 401);
result.put("msg", "用户名或密码错误");
return result;
}
// 生成token
String token = UUID.randomUUID().toString();
redisTemplate.opsForValue().set("token:" + token, user.getId(), 30, TimeUnit.MINUTES);
result.put("code", 200);
result.put("msg", "登录成功");
Map<String, Object> data = new HashMap<>();
data.put("token", token);
data.put("userId", user.getId());
data.put("username", user.getUsername());
result.put("data", data);
} catch (Exception e) {
result.put("code", 500);
result.put("msg", "系统异常");
e.printStackTrace();
}
return result;
}
}
7.2 重构后的代码
@RestController
@RequestMapping("/api/auth")
@Validated
@Api(tags = "认证管理")
@Slf4j
public class AuthController {
private final AuthService authService;
public AuthController(AuthService authService) {
this.authService = authService;
}
@PostMapping("/login")
@ApiOperation("用户登录")
public Result<LoginResponse> login(@RequestBody @Valid LoginRequest request) {
LoginResponse response = authService.login(request);
return Result.success(response);
}
}
@Data
@Builder
public class LoginResponse {
private String token;
private Long userId;
private String username;
}
@Data
public class LoginRequest {
@NotBlank(message = "用户名不能为空")
@Length(min = 4, max = 20, message = "用户名长度必须在4-20个字符之间")
private String username;
@NotBlank(message = "密码不能为空")
@Length(min = 6, max = 20, message = "密码长度必须在6-20个字符之间")
private String password;
}
@Service
@Slf4j
public class AuthServiceImpl implements AuthService {
private final UserService userService;
private final RedisTemplate<String, Object> redisTemplate;
public AuthServiceImpl(UserService userService, RedisTemplate<String, Object> redisTemplate) {
this.userService = userService;
this.redisTemplate = redisTemplate;
}
@Override
public LoginResponse login(LoginRequest request) {
// 业务逻辑在Service层处理
User user = userService.authenticate(request.getUsername(), request.getPassword());
// 生成token
String token = TokenUtils.generateToken();
redisTemplate.opsForValue().set(
"token:" + token,
user.getId(),
Duration.ofMinutes(30)
);
return LoginResponse.builder()
.token(token)
.userId(user.getId())
.username(user.getUsername())
.build();
}
}
八、总结
重构Controller的黄金法则可以总结为以下几点:
- 单一职责:每个Controller只负责一个业务领域
- 统一响应:使用统一的响应格式和自动封装机制
- 优雅校验:使用注解进行参数校验,避免冗余代码
- 全局异常:集中处理异常,提供友好的错误信息
- 合理配置:善用SpringBoot的注解和配置选项
记住,优雅的代码不是一蹴而就的,需要在日常开发中不断重构和优化。当你遵循这些黄金法则时,你会发现Controller代码变得简洁、清晰、易于维护,团队协作也会更加顺畅。
希望今天的分享能帮助你在下次重构Controller时,写出更加优雅的代码!
在实际项目中,建议根据具体业务需求和团队规范来调整这些原则,但核心思想是不变的:让代码更加清晰、可维护、可扩展。
标题:重构Controller的黄金法则:让你的代码优雅如诗!
作者:jiangyi
地址:http://www.jiangyi.space/articles/2025/12/21/1766304302114.html
- 一、Controller的职责定位:守住边界,各司其职
- 1.1 Controller应该做什么
- 1.2 Controller不应该做什么
- 二、黄金法则一:单一职责原则
- 2.1 按业务模块划分
- 2.2 按功能职责划分
- 三、黄金法则二:统一响应格式
- 3.1 定义统一响应类
- 3.2 使用ResponseBodyAdvice自动封装
- 3.3 Controller中的应用
- 四、黄金法则三:优雅的参数校验
- 4.1 使用JSR-303注解校验
- 4.2 分组校验
- 4.3 自定义校验注解
- 五、黄金法则四:全局异常处理
- 5.1 定义统一异常类
- 5.2 全局异常处理器
- 六、黄金法则五:合理使用注解和配置
- 6.1 简化Controller配置
- 6.2 使用构造器注入
- 七、实战案例:重构前后的对比
- 7.1 重构前的代码
- 7.2 重构后的代码
- 八、总结