找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2102

积分

0

好友

298

主题
发表于 2025-12-25 18:54:41 | 查看: 34| 回复: 0

在Spring Boot或Spring MVC应用中,Controller层负责接收和响应请求,其定位如同“不可或缺的配角”。它虽不处理核心业务逻辑,却是系统内外交互的关键枢纽。一个设计优良的Controller层能极大提升代码的可维护性与接口的友好性。

常见的Controller层问题

通常,Controller层需要完成以下几项工作:

  • 接收请求并解析参数
  • 调用Service执行业务逻辑(可能包含参数校验)
  • 捕获业务异常并反馈
  • 业务执行成功后做出响应

参考以下传统代码示例:

// DTO
@Data
public class TestDTO {
    private Integer num;
    private String type;
}

// Service
@Service
public class TestService {
    public Double service(TestDTO testDTO) throws Exception {
        if (testDTO.getNum() <= 0) {
            throw new Exception("输入的数字需要大于0");
        }
        if (testDTO.getType().equals("square")) {
            return Math.pow(testDTO.getNum(), 2);
        }
        if (testDTO.getType().equals("factorial")) {
            double result = 1;
            int num = testDTO.getNum();
            while (num > 1) {
                result = result * num;
                num -= 1;
            }
            return result;
        }
        throw new Exception("未识别的算法");
    }
}

// Controller
@RestController
public class TestController {
    private TestService testService;

    @PostMapping("/test")
    public Double test(@RequestBody TestDTO testDTO) {
        try {
            Double result = this.testService.service(testDTO);
            return result;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    @Autowired
    public void setTestService(TestService testService) {
        this.testService = testService;
    }
}

上述实现方式存在几个明显问题:

  1. 职责耦合:参数校验与业务逻辑混杂在Service中,违背单一职责原则。
  2. 代码重复:相同的异常可能在多处抛出,导致冗余代码。
  3. 响应不统一:成功与失败的响应格式不一致,给接口调用方带来不便。

Controller层的优化实践

1. 统一响应结构

统一的返回结构对于前后端分离项目至关重要,它能明确告知调用方接口的执行状态(成功/失败)及具体信息。

首先,定义状态码接口和常用枚举:

// 定义返回状态接口
public interface IResult {
    Integer getCode();
    String getMessage();
}

// 常用结果枚举
public enum ResultEnum implements IResult {
    SUCCESS(2001, "接口调用成功"),
    VALIDATE_FAILED(2002, "参数校验失败"),
    COMMON_FAILED(2003, "接口调用失败"),
    FORBIDDEN(2004, "没有权限访问资源");
    // 省略构造方法和getter
}

接着,定义统一的响应包装类:

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result<T> {
    private Integer code;
    private String message;
    private T data;

    // 成功静态方法
    public static <T> Result<T> success(T data) {
        return new Result<>(ResultEnum.SUCCESS.getCode(), ResultEnum.SUCCESS.getMessage(), data);
    }
    public static <T> Result<T> success(String message, T data) {
        return new Result<>(ResultEnum.SUCCESS.getCode(), message, data);
    }

    // 失败静态方法
    public static Result<?> failed() {
        return new Result<>(ResultEnum.COMMON_FAILED.getCode(), ResultEnum.COMMON_FAILED.getMessage(), null);
    }
    public static Result<?> failed(String message) {
        return new Result<>(ResultEnum.COMMON_FAILED.getCode(), message, null);
    }
    public static Result<?> failed(IResult errorResult) {
        return new Result<>(errorResult.getCode(), errorResult.getMessage(), null);
    }
}

2. 利用ResponseBodyAdvice实现全局包装

若在每个Controller方法中手动返回Result对象,则会产生大量重复代码。可以利用Spring MVC提供的ResponseBodyAdvice接口进行全局响应包装。

ResponseBodyAdvice允许我们在HttpMessageConverter进行类型转换前,对Controller返回值进行拦截处理。

// 注意:若项目引入了Swagger或Knife4j,basePackages需指定为自己的项目包路径
@RestControllerAdvice(basePackages = "com.example.demo")
public class ResponseAdvice implements ResponseBodyAdvice<Object> {

    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        // 此处可添加过滤逻辑,例如通过注解排除不需要包装的接口
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
                                  Class<? extends HttpMessageConverter<?>> selectedConverterType,
                                  ServerHttpRequest request, ServerHttpResponse response) {
        // 若返回值已被包装,则直接返回
        if (body instanceof Result) {
            return body;
        }
        // 统一包装为成功结果
        return Result.success(body);
    }
}

3. 处理String类型返回值异常

直接使用上述ResponseAdvice处理String类型返回值时,可能抛出类型转换异常。这是因为StringHttpMessageConverter的顺序优先于MappingJackson2HttpMessageConverter

解决方案一:在beforeBodyWrite中手动处理String类型。

@RestControllerAdvice(basePackages = "com.example.demo")
public class ResponseAdvice implements ResponseBodyAdvice<Object> {
    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public Object beforeBodyWrite(Object body, ...) {
        if (body instanceof Result) {
            return body;
        }
        // 对String类型特殊处理
        if (body instanceof String) {
            try {
                return objectMapper.writeValueAsString(Result.success(body));
            } catch (JsonProcessingException e) {
                throw new RuntimeException(e);
            }
        }
        return Result.success(body);
    }
}

// Controller方法需指定produces
@GetMapping(value = "/returnString", produces = "application/json; charset=UTF-8")
public String returnString() {
    return "success";
}

解决方案二(推荐):调整HttpMessageConverter的顺序,将MappingJackson2HttpMessageConverter置前。

@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        for (int i = 0; i < converters.size(); i++) {
            if (converters.get(i) instanceof MappingJackson2HttpMessageConverter) {
                // 将Jackson转换器与首位交换
                MappingJackson2HttpMessageConverter jacksonConverter = (MappingJackson2HttpMessageConverter) converters.get(i);
                converters.set(i, converters.get(0));
                converters.set(0, jacksonConverter);
                break;
            }
        }
    }
}

4. 参数校验解耦

借助JSR 303(Bean Validation)标准及其实现(如Hibernate Validator),可以将参数校验逻辑从业务代码中彻底解耦。

4.1 @PathVariable@RequestParam 校验

在Controller类上标注@Validated,并在参数上直接添加校验注解。

@RestController
@RequestMapping("/example")
@Validated // 开启方法级参数校验
public class ExampleController {

    @GetMapping("/{id}")
    public Integer getDetail(@PathVariable("id") @Min(1) @Max(100) Integer id) {
        return id * id;
    }

    @GetMapping("/search")
    public TestDTO search(@RequestParam @NotBlank @Email String email) {
        TestDTO dto = new TestDTO();
        dto.setEmail(email);
        return dto;
    }
}

校验失败将抛出ConstraintViolationException

4.2 @RequestBody 参数校验

在DTO字段上定义校验规则,并在Controller参数前添加@Validated@Valid注解。

// DTO
@Data
public class TestDTO {
    @NotBlank
    private String userName;

    @NotBlank
    @Length(min = 6, max = 20)
    private String password;

    @NotNull
    @Email
    private String email;
}

// Controller
@RestController
public class TestController {
    @PostMapping("/register")
    public void register(@RequestBody @Validated TestDTO testDTO) {
        // 参数校验通过后,才会执行此方法
        userService.register(testDTO);
    }
}

校验失败将抛出MethodArgumentNotValidException

4.3 自定义校验规则

当标准注解无法满足复杂业务时,可以自定义校验器。

  1. 定义自定义注解
    @Target({ElementType.FIELD, ElementType.PARAMETER})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Constraint(validatedBy = MobileValidator.class) // 指定校验器
    public @interface Mobile {
    boolean required() default true;
    String message() default "手机号格式不正确";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
    }
  2. 实现校验逻辑

    public class MobileValidator implements ConstraintValidator<Mobile, String> {
    private boolean required;
    private final Pattern pattern = Pattern.compile("^1[3-9]\\d{9}$");
    
    @Override
    public void initialize(Mobile constraintAnnotation) {
        this.required = constraintAnnotation.required();
    }
    
    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (!required && !StringUtils.hasText(value)) {
            return true; // 非必填且为空,跳过校验
        }
        return pattern.matcher(value).matches();
    }
    }
  3. 使用自定义注解
    @Data
    public class UserDTO {
    @Mobile
    private String phoneNumber;
    }

5. 统一异常处理

自定义业务异常并结合@RestControllerAdvice进行全局异常拦截,可以返回统一结构的错误信息,并保证HTTP状态码为200(由业务码区分状态)。

  1. 定义业务异常
    // 业务异常
    public class BusinessException extends RuntimeException {
    public BusinessException(String message) {
        super(message);
    }
    }
    // 权限异常
    public class ForbiddenException extends RuntimeException {
    public ForbiddenException(String message) {
        super(message);
    }
    }
  2. 全局异常处理器

    @RestControllerAdvice(basePackages = "com.example.demo")
    public class GlobalExceptionHandler {
    
    @ExceptionHandler(BusinessException.class)
    public Result<?> handleBusinessException(BusinessException ex) {
        return Result.failed(ex.getMessage());
    }
    
    @ExceptionHandler(ForbiddenException.class)
    public Result<?> handleForbiddenException(ForbiddenException ex) {
        return Result.failed(ResultEnum.FORBIDDEN);
    }
    
    // 处理@RequestBody参数校验异常
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result<?> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
        BindingResult bindingResult = ex.getBindingResult();
        StringBuilder sb = new StringBuilder("参数校验失败:");
        for (FieldError error : bindingResult.getFieldErrors()) {
            sb.append(error.getField()).append(": ").append(error.getDefaultMessage()).append("; ");
        }
        return Result.failed(ResultEnum.VALIDATE_FAILED.getCode(), sb.toString());
    }
    
    // 处理@PathVariable和@RequestParam参数校验异常
    @ExceptionHandler(ConstraintViolationException.class)
    public Result<?> handleConstraintViolationException(ConstraintViolationException ex) {
        return Result.failed(ResultEnum.VALIDATE_FAILED.getCode(), ex.getMessage());
    }
    
    // 兜底异常处理
    @ExceptionHandler(Exception.class)
    public Result<?> handleOtherException(Exception ex) {
        // 生产环境可记录日志,返回更通用的错误信息
        return Result.failed("系统繁忙,请稍后重试");
    }
    }

    通过Spring AOP机制,@ExceptionHandler能够优雅地拦截并处理Controller层及其下层抛出的特定异常。

总结

经过上述改造,Controller层的职责变得清晰而纯粹:

  • 参数校验:通过声明式注解完成,与业务逻辑解耦。
  • 响应包装:通过全局切面自动完成,格式统一。
  • 异常处理:通过全局处理器分类处理,反馈友好。

这样的Controller层设计遵循了单一职责原则,提升了代码的可读性与可维护性,使开发者能更专注于核心业务逻辑的实现。




上一篇:Vue.js前后端分离登录框渗透实战:通过信息泄露突破管理员后台
下一篇:Java split方法陷阱解析:CSV数据分割时末尾空字符串丢失问题
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-11 20:16 , Processed in 0.327742 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表