Spring Boot3 中让 Controller 层代码变干净高效的方法

Spring Boot3 中让 Controller 层代码变干净高效的方法

在当今的互联网软件开发领域,Spring Boot 无疑是最受欢迎的框架之一。而在 Spring Boot 3 的项目开发中,Controller 层作为应用与外界交互的重大门户,其代码的干净高效与否,直接影响着整个项目的质量和开发效率。你是否也曾为 Controller 层那繁杂的代码而苦恼?今天,就让我们一起深入探讨如何让 Spring Boot 3 中 Controller 层的代码变得干净又高效。

统一返回结构与包装处理

在项目开发中,无论是前后端分离架构,还是传统架构,统一返回值类型都是极为必要的。它能让对接接口的开发人员,仅通过状态码和状态信息,就能清晰判断接口调用是否成功。否则,仅依据返回值是否为null来判断,在一些特定接口设计中,极易出现误判。例如,某些接口原本设计就是正常返回null,但在不合理的判断逻辑下,可能被误判为调用失败。

统一返回结构后,若在每个 Controller 中都手动编写最终封装逻辑,会产生大量重复代码。Spring 提供的ResponseBodyAdvice类,能完美解决这一问题。ResponseBodyAdvice会在HttpMessageConverter进行类型转换之前,拦截 Controller 返回的内容并进行处理,之后再将结果返回给客户端。如此一来,统一包装的工作便可聚焦在此类中完成。同时,为增加灵活性,可添加校验手段,如添加标记排除注解,对于已包装的body不再重复包装。

不过,在使用ResponseBodyAdvice时,处理字符串类型返回值时会遇到xxx.包装类 cannot be cast to java.lang.String的类型转换异常。经调试发现,String 类型的selectedConverterType参数值为
org.springframework.http.converter.StringHttpMessageConverter,而其他数据类型的值是
org.springframework.http.converter.json.MappingJackson2HttpMessageConverter。缘由在于,我们期望返回Result对象,使用
MappingJackson2HttpMessageConverter可正常转换,而
StringHttpMessageConverter字符串转换器会导致类型转换失败。

解决该问题有两种方式:一是在beforeBodyWrite方法中判断,若返回值为 String 类型,手动将Result对象转换为 JSON 字符串,并在@RequestMapping中指定ContentType;二是调整HttpMessageConverter实例集合中
MappingJackson2HttpMessageConverter的顺序。由于问题根源在于
StringHttpMessageConverter顺序先于
MappingJackson2HttpMessageConverter,将
MappingJackson2HttpMessageConverter顺序提前即可解决。但需注意,直接在集合首位添加
MappingJackson2HttpMessageConverter虽能解决问题,但并非最合理做法,应调整其在集合中的顺序,使其位于
StringHttpMessageConverter之前。

下面是统一返回结构及ResponseBodyAdvice处理的示例代码:

// 统一返回结果类
public class Result<T> {
    private int code;
    private String message;
    private T data;

    // 构造方法、getter和setter省略

    public static <T> Result<T> success(T data) {
        Result<T> result = new Result<>();
        result.setCode(200);
        result.setMessage("成功");
        result.setData(data);
        return result;
    }

    public static <T> Result<T> fail(int code, String message) {
        Result<T> result = new Result<>();
        result.setCode(code);
        result.setMessage(message);
        return result;
    }
}

// 统一返回处理类
@RestControllerAdvice
public class ResponseBodyHandler implements ResponseBodyAdvice<Object> {

    @Autowired
    private ObjectMapper objectMapper;

    @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) {
        // 如果已经是Result类型,直接返回
        if (body instanceof Result) {
            return body;
        }

        // 处理String类型
        if (body instanceof String) {
            try {
                response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
                return objectMapper.writeValueAsString(Result.success(body));
            } catch (JsonProcessingException e) {
                e.printStackTrace();
            }
        }

        // 其他类型统一包装
        return Result.success(body);
    }
}

参数校验优化

参数校验是 Controller 层的重大工作之一,但传统方式将参数校验与业务代码过度耦合,违背了单一职责原则。Java API 规范JSR303定义的校验标准validation – api,以及其知名实现hibernate validation,还有 Spring 对其进行二次封装的spring validation,为我们提供了更好的解决方案。在 SpringMVC 中,这些校验机制可实现参数自动校验,让参数校验代码与业务逻辑代码解耦。

对于@PathVariable和@RequestParam参数,在入参处声明约束注解,即可轻松实现校验。一旦校验失败,会抛出
MethodArgumentNotValidException异常。在实际项目中,若 Get 请求参数较多,思考到 url 长度限制和代码可维护性,超过 5 个参数时,提议使用实体传参。此外,Spring Boot 3 对参数校验性能进行了优化,采用更高效的验证算法,处理大量参数校验时,相比 Spring Boot 2,性能提升约 30%。同时,Spring Boot 3 还支持分组校验,例如在用户注册和登录场景中,注册时需校验更多参数,登录时仅需校验用户名和密码,通过定义不同分组,可在不同场景下针对性校验。

以下是参数校验的示例代码:

// 分组接口
public interface Group {
    interface Add {}
    interface Update {}
}

// 实体类
public class User {
    @Null(message = "id必须为null", groups = Group.Add.class)
    @NotNull(message = "id不能为空", groups = Group.Update.class)
    private Long id;

    @NotBlank(message = "用户名不能为空")
    @Size(min = 2, max = 20, message = "用户名长度必须在2-20之间")
    private String username;

    @NotBlank(message = "密码不能为空")
    @Pattern(regexp = "^(?=.*[0-9])(?=.*[a-zA-Z])(?=.*[@#$%^&*])[0-9a-zA-Z@#$%^&*]{8,20}$", message = "密码必须包含数字、字母和特殊字符,长度8-20")
    private String password;

    // getter和setter省略
}

// Controller层
@RestController
@RequestMapping("/user")
public class UserController {

    @PostMapping("/add")
    public Result<User> addUser(@Validated(Group.Add.class) @RequestBody User user) {
        // 业务逻辑处理
        return Result.success(user);
    }

    @PutMapping("/update")
    public Result<User> updateUser(@Validated(Group.Update.class) @RequestBody User user) {
        // 业务逻辑处理
        return Result.success(user);
    }

    @GetMapping("/get")
    public Result<User> getUser(@NotNull(message = "id不能为空") @RequestParam Long id) {
        // 业务逻辑处理
        User user = new User();
        user.setId(id);
        user.setUsername("test");
        user.setPassword("123456");
        return Result.success(user);
    }

    // 全局异常处理参数校验异常
    @RestControllerAdvice
    public static class GlobalExceptionHandler {
        @ExceptionHandler(MethodArgumentNotValidException.class)
        public Result<Void> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
            String message = e.getBindingResult().getFieldError().getDefaultMessage();
            return Result.fail(400, message);
        }

        @ExceptionHandler(ConstraintViolationException.class)
        public Result<Void> handleConstraintViolationException(ConstraintViolationException e) {
            String message = e.getConstraintViolations().iterator().next().getMessage();
            return Result.fail(400, message);
        }
    }
}

使用@ControllerAdvice实现全局处理

@ControllerAdvice注解堪称 Spring 提供的强劲工具,它能实现全局范围内的控制器逻辑增强,广泛应用于全局异常处理、全局数据绑定和全局权限校验等场景。

在全局异常处理方面,通过@ControllerAdvice结合@ExceptionHandler,可轻松定义全局异常处理类。在此类中,能针对不同类型异常,如空指针异常、数据越界异常、非法参数异常等,分别定义处理方法。异常处理优先级为 Controller 局部处理器高于全局处理器。当发生异常时,系统会先查找 Controller 内的局部处理器,若未找到,则由全局处理器处理。

在全局数据绑定方面,借助@ControllerAdvice,可将一些公共数据定义在该注解标记的类中,使每个 Controller 接口都能访问这些数据。例如,在多实体类存在一样属性名时,可通过@ControllerAdvice的全局数据预处理功能,结合@InitBinder注解,为不同实体类参数添加前缀,实现参数区分,避免前端传递参数时的混淆。

以下是@ControllerAdvice实现全局处理的示例代码:

@ControllerAdvice
public class GlobalControllerAdvice {

    // 全局异常处理
    @ExceptionHandler(NullPointerException.class)
    @ResponseBody
    public Result<Void> handleNullPointerException(NullPointerException e) {
        return Result.fail(500, "发生空指针异常:" + e.getMessage());
    }

    @ExceptionHandler(IndexOutOfBoundsException.class)
    @ResponseBody
    public Result<Void> handleIndexOutOfBoundsException(IndexOutOfBoundsException e) {
        return Result.fail(500, "发生数据越界异常:" + e.getMessage());
    }

    // 全局数据绑定
    @ModelAttribute
    public void addAttributes(Model model) {
        model.addAttribute("systemName", "Spring Boot 3 Demo");
        model.addAttribute("version", "1.0.0");
    }

    // 全局数据预处理
    @InitBinder("user")
    public void initUserBinder(WebDataBinder binder) {
        binder.setFieldDefaultPrefix("user.");
    }

    @InitBinder("order")
    public void initOrderBinder(WebDataBinder binder) {
        binder.setFieldDefaultPrefix("order.");
    }
}

// Controller层使用全局数据
@RestController
@RequestMapping("/demo")
public class DemoController {

    @GetMapping("/info")
    public Result<Map<String, Object>> getInfo(Model model) {
        Map<String, Object> data = new HashMap<>();
        data.put("systemName", model.getAttribute("systemName"));
        data.put("version", model.getAttribute("version"));
        return Result.success(data);
    }

    @PostMapping("/save")
    public Result<Void> save(@ModelAttribute("user") User user, @ModelAttribute("order") Order order) {
        // 业务逻辑处理
        return Result.success(null);
    }
}

使用@SuperController注解(如有)

如果项目中引入了@SuperController注解,那将是提升 Controller 层开发效率的一大助力。@SuperController注解整合了参数校验、接口文档生成、权限校验、异常处理等多种常用功能。

在参数校验上,它可结合自定义校验注解,实现复杂业务规则的校验,如手机号格式、身份证号校验等。通过实现ConstraintValidator接口自定义校验逻辑,以手机号校验为例,自定义@PhoneNumber注解,在实现类中利用正则表达式^1(3 – 9)d{9}$验证手机号格式。在接口文档生成方面,它能自动提取接口信息,生成 Swagger 文档,并支持自定义文档模板,让接口文档更规范美观。

权限校验上,支持声明式权限配置,开发者只需在 Controller 方法上指定所需权限,如”user:read” “admin:write”等,框架便会在请求处理前自动校验,校验方式可基于角色访问控制(RBAC)或权限点细粒度控制。异常处理方面,它提供统一机制,全局捕获 Controller 层抛出的异常,根据异常类型分类处理,返回不同错误码和信息,方便前端提示和后端排查问题。使用@SuperController注解后,Controller 方法核心业务逻辑更加突出,开发效率大幅提升,代码结构也更加清晰。

以下是使用@SuperController注解及自定义校验的示例代码(假设@SuperController已由相关框架提供):

// 自定义手机号校验注解
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneNumberValidator.class)
public @interface PhoneNumber {
    String message() default "手机号格式不正确";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

// 手机号校验实现类
public class PhoneNumberValidator implements ConstraintValidator<PhoneNumber, String> {
    private static final Pattern PHONE_PATTERN = Pattern.compile("^1(3-9)d{9}$");

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (value == null) {
            return true; // 允许为空,若不允许为空可添加@NotBlank注解
        }
        return PHONE_PATTERN.matcher(value).matches();
    }
}

// 实体类使用自定义注解
public class UserInfo {
    @NotBlank(message = "姓名不能为空")
    private String name;

    @PhoneNumber
    private String phone;

    // getter和setter省略
}

// 使用@SuperController注解的Controller
@SuperController
@RequestMapping("/userInfo")
public class UserInfoController {

    @PostMapping("/save")
    @RequiresPermissions("user:save") // 权限校验
    public Result<UserInfo> saveUserInfo(@Valid @RequestBody UserInfo userInfo) {
        // 业务逻辑处理
        return Result.success(userInfo);
    }
}

总结

在 Spring Boot 3 的开发中,通过上述方法对 Controller 层代码进行优化,能让代码更加干净、高效,提升项目整体质量和开发效率。希望本文介绍的这些方法能协助各位开发者在日常开发中事半功倍,打造出更优质的互联网软件项目。让我们一起在代码的世界里不断探索,追求卓越的代码质量!

© 版权声明
THE END
如果内容对您有所帮助,就支持一下吧!
点赞0 分享
评论 共5条

请登录后发表评论