当前位置:首页 > 技术文章 > 正文内容

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

zonemu2个月前 (08-24)技术文章27

在当今的互联网软件开发领域,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}#34;, 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}#34;);

    @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 层代码进行优化,能让代码更加干净、高效,提升项目整体质量和开发效率。希望本文介绍的这些方法能帮助各位开发者在日常开发中事半功倍,打造出更优质的互联网软件项目。让我们一起在代码的世界里不断探索,追求卓越的代码质量!

相关文章

Vue3 如何实现父子组件传值?(vue父子组件传值props)

在Vue 3中,要实现父子组件传值效果主要通过props和emit两种机制来实现,下面我们就来详细介绍一下这两种机制。父组件向子组件传值propsprops是Vue组件的一种机制,主要的作用就是实现从...

Vue2的16种传参通信方式(vue传参数)

前言先直入主题列出有哪些传参方式,下面再通过事例一一讲解。props(父传子)$emit与v-on (子传父)EventBus (兄弟传参).sync与update: (父子双向)v-model (父...

配置GitLab流水线和门禁系统(gitlab工作流)

在项目开发的过程中,为了保证代码质量,我们会使用诸多代码质量检测工具,这些工具或是在本地,或是在云端,虽然工具可以检测出异常问题,但是这些问题还是需要我们程序员来修复,如果我们不强制所有人必须修复异常...

程序员项目经理如何调动组员积极性

#这个方法应该很适合程序员都说程序员是比较傲娇,有点小自负(有的是相当,那不叫自负,那是实力的体现好吗),略微呆萌,自尊心偏小强的一类族群。是吗?中招了吗?作为管理好几个组员,要完成一个大项目的项目经...

HTML5学习笔记三:HTML5语法规则(html5语法详解)

1.标签要小写2.属性值可加可不加””或”3.可以省略某些标签 html body head tbody4.可以省略某些结束标签 tr td li例:显示效果:5.单标签不用加结束标签img inpu...

Web开发的十佳HTML5响应式框架(h5响应式模板)

HTML5框架是一类有助于快速轻松创建响应式网站的程序包。这些HTML5框架有着能减轻编程任务和重复代码负担的神奇功能。关于HTML5的框架种类繁多,并且很瘦欢迎,因为它能允许开发人员花费更少的时间和...