springboot 参数校验

本贴最后更新于 2569 天前,其中的信息可能已经时移世改

本文项目已发布到 github,后续学习项目也会添加到此工程下,欢迎 fork 点赞。
https://github.com/wangyuheng/spring-boot-sample

http 接口开发过程中的常用场景为,根据提交的表单数据进行格式校验,包括字段长度、数据类型、范围等等。。如果每次都写一堆 if...else if... 太傻了,所以 java 提供了一套标准化校验方案 JSR 303,而标准的最佳实践为 Hibernate Validator

一句话为,通过注解对 bean 进行校验,并返回标准文案。

依赖

spring-boot-starter-web 已经集成了 hibernate-validator, 无需重复引用。

注解

JSR303 定义了一套,在 javax.validation.constraints 包目录下,hibernate-validator 扩展了一套,在 org.hibernate.validator.constraints 包下,下文会介绍如何自定义注解。

在 Bean 的字段中增加注解,代码如下:

    public class User implements Serializable {
    
        @Min(1)
        private int id;
        @Email
        private String username;
        @NotBlank
        @Length(min = 6, max = 36)
        private String password;
        private Integer age;
    
        public int getId() {
            return id;
        }
    
        public void setId(int id) {
            this.id = id;
        }
    
        public String getUsername() {
            return username;
        }
    
        public void setUsername(String username) {
            this.username = username;
        }
    
        public String getPassword() {
            return password;
        }
    
        public void setPassword(String password) {
            this.password = password;
        }
    
        public Integer getAge() {
            return age;
        }
    
        public void setAge(Integer age) {
            this.age = age;
        }
    }

Controller

  1. @Validated 声明对参数进行校验
  2. MessageSource 用来分离错误提示,也可以实现国际化
  3. BindingResult 绑定参数校验结果
    @RestController
    @RequestMapping("/user")
    public class UserApi {
    
        @Autowired
        private MessageSource messageSource;
    
        @GetMapping("/{id}")
        public Object getInfo(@Validated User user, BindingResult bindingResult) {
            Map<String, Object> result = new HashMap<>();
            if (bindingResult.hasErrors()) {
                List<FieldError> fieldErrors = bindingResult.getFieldErrors();
                Locale locale = LocaleContextHolder.getLocale();
                StringBuilder errorMessage = new StringBuilder();
                fieldErrors.forEach(fieldError -> {
                    errorMessage.append(fieldError.getField())
                            .append(":")
                            .append(messageSource.getMessage(fieldError, locale))
                            .append(",");
                });
                result.put("code", 10001);
                result.put("message", errorMessage);
            } else {
                result.put("code", 10000);
                result.put("data", user);
            }
            return result;
        }
    
    }

自定义错误提示

框架本身做了错误提示,但是为了友好,通常会自定义提示。

硬编码

可以在 注解中硬编码提示语,如

    @Email(message = "用户名必须是邮箱")
    private String username;

ValidationMessages.properties

不过不够灵活。在使用 spring-boot 的过程中,我们都熟悉了约定大于配置。可以在 resources 目录下增加 ValidationMessages.properties 文件,并在其中复写

    javax.validation.constraints.Min.message=参数最小为{1}

可以实现自定义提示,注意 properties 中文编码问题。@ 注 1 propertiesEditor

messages.properties

springboot 提供的消息文件默认路径为 resources 下 messages.properties,可以把 ValidationMessages.properties 和 messages.properties 指定为自定义配置文件

  • application.properties 中配置属性
    spring.messages.basename=valid
  • 在 resources 目录下创建校验提示文件 valid.properties
    org.hibernate.validator.constraints.NotBlank.message={0} can't be blank
    user.id.error={0} error
  • 配置 messageSource
    @Configuration
    public class ValidatorConf {
    
        @Autowired
        private MessageSource messageSource;
    
        @Bean
        @ConditionalOnClass(MessageSource.class)
        public LocalValidatorFactoryBean localValidatorFactoryBean() {
            LocalValidatorFactoryBean bean = new LocalValidatorFactoryBean();
            bean.setProviderClass(HibernateValidator.class);
            bean.setValidationMessageSource(messageSource);
            return bean;
        }
    }

简化

需要的校验功能实现了,但是每个 restful 接口都需要增加这些校验代码?每个参数后面都加一个 BindingResult? 显然不合理。懒惰是美德,试着做一次简化。

通过 @RestControllerAdvice@ExceptionHandler 全局捕获 restapi 的 BindException 异常,在 controller 代码中,在需要校验的参数前增加 @Validated

@RestControllerAdvice
public class GlobalValidator {

    @Autowired
    private MessageSource messageSource;

    @ExceptionHandler(BindException.class)
    public Object bindError(BindException e) {
        Map<String, Object> result = new HashMap<>();
        List<FieldError> fieldErrors = e.getFieldErrors();
        Locale locale = LocaleContextHolder.getLocale();
        StringBuilder errorMessage = new StringBuilder();
        fieldErrors.forEach(fieldError -> {
            errorMessage.append(fieldError.getField())
                    .append(":")
                    .append(messageSource.getMessage(fieldError, locale))
                    .append(",");
        });
        result.put("code", 10001);
        result.put("message", errorMessage);
        return result;
    }
}
@RestController
@RequestMapping("/simpleuser")
public class SimpleUserApi {

    @GetMapping("/{uid}")
    public Object getInfo(@Validated User user) {
        Map<String, Object> result = new HashMap<>();
        result.put("code", 10000);
        result.put("data", user);
        return result;
    }

}

测试

mockMvc 请求 api 接口,判断返回值 code

@RunWith(SpringRunner.class)
@SpringBootTest
public class ValidatorApiTest {

    private MockMvc mockMvc;

    @Autowired
    private WebApplicationContext context;

    @Before
    public void setup() {
        mockMvc = MockMvcBuilders.webAppContextSetup(context).build();
    }

    private JSONObject requestApi(String path) throws Exception {
        return new JSONObject(mockMvc.perform(MockMvcRequestBuilders.get(path))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andDo(MockMvcResultHandlers.print())
                .andReturn()
                .getResponse()
                .getContentAsString());
    }

    @Test
    public void test_user_param_valid() throws Exception {
        String rightPath = "/user/" + "12?id=123" + "&password=123456";
        assertTrue(10000 == requestApi(rightPath).getInt("code"));
        String errorPath = "/user/" + "abc";
        assertTrue(10001 == requestApi(errorPath).getInt("code"));
    }


    @Test
    public void test_simpleuser_param_valid() throws Exception {
        String rightPath = "/simpleuser/" + "12?id=123" + "&password=123456";
        assertTrue(10000 == requestApi(rightPath).getInt("code"));
        String errorPath = "/simpleuser/" + "abc";
        assertTrue(10001 == requestApi(errorPath).getInt("code"));
    }

}
  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3190 引用 • 8214 回帖 • 1 关注
  • Spring

    Spring 是一个开源框架,是于 2003 年兴起的一个轻量级的 Java 开发框架,由 Rod Johnson 在其著作《Expert One-On-One J2EE Development and Design》中阐述的部分理念和原型衍生而来。它是为了解决企业应用开发的复杂性而创建的。框架的主要优势之一就是其分层架构,分层架构允许使用者选择使用哪一个组件,同时为 JavaEE 应用程序开发提供集成的框架。

    943 引用 • 1460 回帖 • 3 关注
  • valid
    1 引用

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...