重构Controller的黄金法则:让你的代码优雅如诗!

重构Controller的黄金法则:让你的代码优雅如诗!

作为一名资深后端开发,你有没有遇到过这样的场景:接手一个老项目,打开Controller文件,密密麻麻的代码让人眼花缭乱,业务逻辑和控制逻辑混在一起,异常处理到处都是try-catch,返回格式五花八门...

今天就来聊聊如何重构Controller,让你的代码优雅如诗,告别那些让人头疼的"意大利面条式"代码!

一、Controller的职责定位:守住边界,各司其职

在开始重构之前,我们先要明确Controller的职责边界。一个好的Controller应该像一个优秀的项目经理,只负责协调和调度,而不应该亲自下场搬砖。

1.1 Controller应该做什么

  1. 接收请求:解析HTTP请求参数
  2. 参数校验:验证请求参数的合法性
  3. 调用服务:将请求转发给相应的Service层
  4. 返回响应:将处理结果封装成统一格式返回

1.2 Controller不应该做什么

  1. 业务逻辑:复杂的业务计算、数据处理
  2. 数据持久化:直接操作数据库
  3. 异常处理:到处写try-catch
  4. 工具方法:字符串处理、日期转换等

记住一句话: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的黄金法则可以总结为以下几点:

  1. 单一职责:每个Controller只负责一个业务领域
  2. 统一响应:使用统一的响应格式和自动封装机制
  3. 优雅校验:使用注解进行参数校验,避免冗余代码
  4. 全局异常:集中处理异常,提供友好的错误信息
  5. 合理配置:善用SpringBoot的注解和配置选项

记住,优雅的代码不是一蹴而就的,需要在日常开发中不断重构和优化。当你遵循这些黄金法则时,你会发现Controller代码变得简洁、清晰、易于维护,团队协作也会更加顺畅。

希望今天的分享能帮助你在下次重构Controller时,写出更加优雅的代码!

在实际项目中,建议根据具体业务需求和团队规范来调整这些原则,但核心思想是不变的:让代码更加清晰、可维护、可扩展。


标题:重构Controller的黄金法则:让你的代码优雅如诗!
作者:jiangyi
地址:http://www.jiangyi.space/articles/2025/12/21/1766304302114.html

    0 评论
avatar