Spring BootSpringBoot参数验证以及实现原理

Posted 逆流°只是风景-bjhxcc

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring BootSpringBoot参数验证以及实现原理相关的知识,希望对你有一定的参考价值。

文章目录

前言

参数验证很重要,是平时开发环节中不可少的一部分,但是我想很多后端同事会偷懒,干脆不做,这样很可能给系统的稳定性和安全性带来严重的危害。
那么在Spring Boot应用中如何做好参数校验工作呢,本文提供了小技巧以及验证实现原理,你知道几个呢?

SpringBoot参数验证技巧(12个技巧)

一、使用验证注解

Spring Boot 提供了内置的验证注解,可以帮助简单、快速地对输入字段进行验证,例如检查 null 或空字段、强制执行长度限制、使用正则表达式验证模式以及验证电子邮件地址。

  • 一些最常用的验证注释包括:

    • @NotNull:指定字段不能为空。
    • @NotEmpty:指定列表字段不能为空。
    • @NotBlank:指定字符串字段不得为空或仅包含空格。
    • @Min 和 @Max:指定数字字段的最小值和最大值。
    • @Pattern:指定字符串字段必须匹配的正则表达式模式。
    • @Email:指定字符串字段必须是有效的电子邮件地址。
  • 具体用法参考下面例子:

public class User 
    @NotNull
    private Long id;

    @NotBlank
    @Size(min = 2, max = 50)
    private String firstName;

    @NotBlank
    @Size(min = 2, max = 50)
    private String lastName;

    @Email
    private String email;

    @NotNull
    @Min(18)
    @Max(99)
    private Integer age;

    @NotEmpty
    private List<String> hobbies;

    @Pattern(regexp = "[A-Z]2\\d4")
    private String employeeId;
 

二、使用自定义验证注解

虽然 Spring Boot 的内置验证注释很有用,但它们可能无法涵盖所有情况。如果有特殊参数验证的场景,可以使用 Spring 的 JSR 303 验证框架创建自定义验证注释。自定义注解可以让你的的验证逻辑更具可重用性和可维护性。

假设我们有一个应用程序,用户可以在其中创建帖子。每个帖子都应该有一个标题和一个正文,并且标题在所有帖子中应该是唯一的。虽然 Spring Boot 提供了用于检查字段是否为空的内置验证注释,但它没有提供用于检查唯一性的内置验证注释。在这种情况下,我们可以创建一个自定义验证注解来处理这种情况。

  • 首先,我们创建自定义约束注解UniqueTitle :
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = UniqueTitleValidator.class)
public @interface UniqueTitle 
    String message() default "Title must be unique";

    Class<?>[] groups() default ;

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

  • 接下来,我们创建一个PostRepository接口,目的是从数据库中检索帖子:
public interface PostRepository extends JpaRepository<Post, Long> 
    Post findByTitle(String title);

  • 然后我们需要定义验证器类 UniqueTitleValidator,如下所示:
@Component
public class UniqueTitleValidator implements ConstraintValidator<UniqueTitle, String> 

    @Autowired
    private PostRepository postRepository;

    @Override
    public boolean isValid(String title, ConstraintValidatorContext context) 
        if (title == null) 
            return true;
        
        return Objects.isNull(postRepository.findByTitle(title));
    

UniqueTitleValidator实现了ConstraintValidator接口,它有两个泛型类型:
第一个是自定义注解UniqueTitle
第二个是正在验证的字段类型(在本例中为String). 我们还自动装配了PostRepository 类以从数据库中检索帖子。
isValid()方法通过查询 PostRepository 来检查 title 是否为 null 或者它是否是唯一的。如果 title 为 null 或唯一,则验证成功,并返回 true。

  • 最后定义了自定义验证注释和验证器类后,我们现在可以使用它来验证 Spring Boot 应用程序中的帖子标题:
public class Post 
    @UniqueTitle
    private String title;

    @NotNull
    private String body;

我们已将 @UniqueTitle 注释应用于 Post 类中的 title 变量。验证此字段时,这将触发 UniqueTitleValidator 类中定义的验证逻辑。

三、在服务器端验证

除了前端或者客户端做了验证意外,服务器端验证输入是至关重要的。它可以确保在处理或存储任何恶意或格式错误的数据之前将其捕获,这对于应用程序的安全性和稳定性至关重要。
假设我们有一个允许用户创建新帐户的 REST 端点。端点需要一个包含用户用户名和密码的 JSON 请求体。为确保输入有效,我们可以创建一个 DTO(数据传输对象)类并将验证注释应用于其字段:

public class UserDTO 

    @NotBlank
    private String username;

    @NotBlank
    private String password;

我们使用@NotBlank注解来确保username和password字段不为空或 null。
接下来,我们可以创建一个控制器方法来处理 HTTP POST 请求并在创建新用户之前验证输入:

@RestController
@RequestMapping("/users")
@Validated
public class UserController 
    @Autowired
    private UserService userService;

    @PostMapping
    public ResponseEntity<String> createUser(@Valid @RequestBody UserDTO userDto) 
        userService.createUser(userDto);
        return ResponseEntity.status(HttpStatus.CREATED).body("User created successfully");
    

我们使用 Spring 的@Validated注解来启用方法级验证,我们还将 @Valid 注释应用于 userDto 参数以触发验证过程。

四、提供有意义的错误信息

当验证失败时,必须提供清晰简洁的错误消息来描述出了什么问题以及如何修复它。

这是一个示例,如果我们有一个允许用户创建新用户的 RESTful API。我们要确保姓名和电子邮件地址字段不为空,年龄在 18 到 99 岁之间,除了这些字段,如果用户尝试使用重复的“用户名”创建帐户,我们还会提供明确的错误消息或“电子邮件”。
为此,我们可以定义一个带有必要验证注释的模型类 User,如下所示:

public class User 

    @NotBlank(message = "用户名不能为空")
    private String name;

    @NotBlank(message = "Email不能为空")
    @Email(message = "无效的Emaild地址")
    private String email;

    @NotNull(message = "年龄不能为空")
    @Min(value = 18, message = "年龄必须大于18")
    @Max(value = 99, message = "年龄必须小于99")
    private Integer age;

我们使用 message属性为每个验证注释提供了自定义错误消息。

接下来,在我们的 Spring 控制器中,我们可以处理表单提交并使用 @Valid 注释验证用户输入:

@RestController
@RequestMapping("/users")
public class UserController 
    @Autowired
    private UserService userService;

    @PostMapping
    public ResponseEntity<String> createUser(@Valid @RequestBody User user, BindingResult result) 
        if (result.hasErrors()) 
            List<String> errorMessages = result.getAllErrors().stream()
                    .map(DefaultMessageSourceResolvable::getDefaultMessage)
                    .collect(Collectors.toList());
            return ResponseEntity.badRequest().body(errorMessages.toString());
        

        // save the user to the database using UserService
        userService.saveUser(user);

        return ResponseEntity.status(HttpStatus.CREATED).body("User created successfully");
    

我们使用 @Valid 注释来触发 User 对象的验证,并使用 BindingResult 对象来捕获任何验证错误。

五、将 i18n 用于错误消息

如果你的应用程序支持多种语言,则必须使用国际化 (i18n) 以用户首选语言显示错误消息。
以下是在 Spring Boot 应用程序中使用 i18n 处理错误消息的示例

  • 首先,在资源目录下创建一个包含默认错误消息的 messages.properties 文件
# messages.properties
user.name.required=Name is required.
user.email.invalid=Invalid email format.
user.age.invalid=Age must be a number between 18 and 99.
  • 接下来,为每种支持的语言创建一个 messages_xx.properties 文件,例如,中文的 messages_zh_CN.properties。
user.name.required=名称不能为空.
user.email.invalid=无效的email格式.
user.age.invalid=年龄必须在1899岁之间.
  • 然后,更新您的验证注释以使用本地化的错误消息
public class User 
    @NotNull(message = "user.id.required")
    private Long id;

    @NotBlank(message = "user.name.required")
    private String name;

    @Email(message = "user.email.invalid")
    private String email;

    @NotNull(message = "user.age.required")
    @Min(value = 18, message = "user.age.invalid")
    @Max(value = 99, message = "user.age.invalid")
    private Integer age;

  • 最后,在Spring 配置文件中配置 MessageSource bean 以加载 i18n 消息文件
@Configuration
public class AppConfig 
    @Bean
    public MessageSource messageSource() 
        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
        messageSource.setBasename("messages");
        messageSource.setDefaultEncoding("UTF-8");
        return messageSource;
    

    @Bean
    public LocalValidatorFactoryBean validator() 
        LocalValidatorFactoryBean validatorFactoryBean = new LocalValidatorFactoryBean();
        validatorFactoryBean.setValidationMessageSource(messageSource());
        return validatorFactoryBean;
    

现在,当发生验证错误时,错误消息将根据随请求发送的“Accept-Language”标头以用户的首选语言显示。

六、使用分组验证

验证组是 Spring Boot 验证框架的一个强大功能,允许您根据其他输入值或应用程序状态应用条件验证规则。

现在有一个包含三个字段的User类的情况下:firstName、lastName和email。我们要确保如果 email 字段为空,则 firstName 或 lastName 字段必须非空。否则,所有三个字段都应该正常验证。

为此,我们将定义两个验证组:EmailNotEmpty 和 Default。EmailNotEmpty 组将包含当 email 字段不为空时的验证规则,而 Default 组将包含所有三个字段的正常验证规则。

创建带有验证组的 User 类

public class User 
    @NotBlank(groups = Default.class)
    private String firstName;

    @NotBlank(groups = Default.class)
    private String lastName;

    @Email(groups = EmailNotEmpty.class)
    private String email;

    // getters and setters omitted for brevity
    public interface EmailNotEmpty 
    public interface Default 

请注意,我们在User类中定义了两个接口,EmailNotEmpty和 Default。这些将作为我们的验证组。

接下来,我们更新Controller使用这些验证组

@RestController
@RequestMapping("/users")
@Validated
public class UserController 
    public ResponseEntity<String> createUser(
            @Validated(org.example.model.ex6.User.EmailNotEmpty.class) @RequestBody User userWithEmail,
            @Validated(User.Default.class) @RequestBody User userWithoutEmail)
    
        // Create the user and return a success response
       
    

我们已将@Validated注释添加到我们的控制器,表明我们想要使用验证组。我们还更新了 createUser 方法,将两个 User 对象作为输入,一个在 email 字段不为空时使用,另一个在它为空时使用。
@Validated 注释用于指定将哪个验证组应用于每个 User 对象。对于 userWithEmail 参数,我们指定了 EmailNotEmpty 组,而对于 userWithoutEmail 参数,我们指定了 Default 组。

进行这些更改后,现在将根据“电子邮件”字段是否为空对“用户”类进行不同的验证。如果为空,则 firstName 或 lastName 字段必须非空。否则,所有三个字段都将正常验证。

七、对复杂逻辑使用跨域验证

如果需要验证跨多个字段的复杂输入规则,可以使用跨字段验证来保持验证逻辑的组织性和可维护性。跨字段验证可确保所有输入值均有效且彼此一致,从而防止出现意外行为。

假设我们有一个表单,用户可以在其中输入任务的开始日期和结束日期,并且我们希望确保结束日期不早于开始日期。我们可以使用跨域验证来实现这一点。

  • 首先,我们定义一个自定义验证注解EndDateAfterStartDate:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = EndDateAfterStartDateValidator.class)
public @interface EndDateAfterStartDate 
    String message() default "End date must be after start date";
    Class<?>[] groups() default ;
    Class<? extends Payload>[] payload() default ;

  • 然后,我们创建验证器EndDateAfterStartDateValidator:
public class EndDateAfterStartDateValidator implements ConstraintValidator<EndDateAfterStartDate, TaskForm> 
    @Override
    public boolean isValid(TaskForm taskForm, ConstraintValidatorContext context) 
        if (taskForm.getStartDate() == null || taskForm.getEndDate() == null) 
            return true;
        

        return taskForm.getEndDate().isAfter(taskForm.getStartDate());
    

  • 最后,我们将EndDateAfterStartDate注释应用于我们的表单对象TaskForm:
@EndDateAfterStartDate
public class TaskForm 
    @NotNull
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate startDate;

    @NotNull
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate endDate;

现在,当用户提交表单时,验证框架将自动检查结束日期是否晚于开始日期,如果不是,则提供有意义的错误消息。

八、对验证错误使用异常处理

可以使用异常处理ExceptionHandler来统一捕获和处理验证错误。
以下是如何在 Spring Boot 中使用异常处理来处理验证错误的示例:

@RestControllerAdvice
public class RestExceptionHandler extends ResponseEntityExceptionHandler 

    @ExceptionHandler(MethodArgumentNotValidException.class)
    protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex,
                                                                  HttpHeaders headers, HttpStatus status,
                                                                  WebRequest request) 
        Map<String, Object> body = new LinkedHashMap<>();
        body.put("timestamp", LocalDateTime.now());
        body.put("status", status.value());

        // Get all errors
        List<String> errors = ex.getBindingResult()
                .getFieldErrors()
                .stream()
                .map(x -> x.getDefaultMessage())
                .collect(Collectors.toList());

        body.put("errors", errors);

        return new ResponseEntity<>(body, headers, status);
    

  • 在这里,我们创建了一个用 @RestControllerAdvice 注解的 RestExceptionHandler 类来处理我们的 REST API 抛出的异常。

  • 然后我们创建一个用 @ExceptionHandler 注解的方法来处理在验证失败时抛出的 MethodArgumentNotValidException。
    在处理程序方法中,我们创建了一个 Map 对象来保存错误响应的详细信息,包括时间戳、HTTP 状态代码和错误消息列表。我们使用 MethodArgumentNotValidException 对象的 getBindingResult() 方法获取所有验证错误并将它们添加到错误消息列表中。

  • 最后,我们返回一个包含错误响应详细信息的ResponseEntity对象,包括作为响应主体的错误消息列表、HTTP 标头和 HTTP 状态代码。
    有了这个异常处理代码,我们的 REST API 抛出的任何验证错误都将被捕获并以结构化和有意义的格式返回给用户,从而更容易理解和解决问题。

九、测试你的验证逻辑

需要为你的验证逻辑编写单元测试,以帮助确保它正常工作。

@DataJpaTest
public class UserValidationTest 

    @Autowired
    private TestEntityManager entityManager;

    @Autowired
    private Validator validator;

    @Test
    public void testValidation() 
        User user = new User();
        user.setFirstName("John");
        user.setLastName("Doe");
        user.setEmail("invalid email");

        Set<ConstraintViolation<User>> violations = validator.validate(user);

        assertEquals(1, violations.size());
        assertEquals("must be a well-formed email address", violations.iterator().next().getMessage());
    

我们使用 JUnit 5 编写一个测试来验证具有无效电子邮件地址的“用户”对象。然后我们使用 Validator 接口来验证 User 对象并检查是否返回了预期的验证错误。

十、 PathVariable校验

@GetMapping("/path/group:[a-zA-Z0-9_]+/userid")
@ResponseBody
public String path(@PathVariable("group") String group, @PathVariable("userid") Integer userid) 
    return group + ":" + userid;


用法是:路径变量:正则表达式。当请求URI不满足正则表达式时,客户端将收到404错误码。不方便的地方是,不能通过捕获异常的方式,向前端返回统一的、自定义格式的响应参数。

十一、方法参数校验

@GetMapping("/validate1")
@ResponseBody
public String validate1(
        @Size(min = 1,max = 10,message = "姓名长度必须为1到10")@RequestParam("name") String name,
        @Min(value = 10,message = "年龄最小为10")@Max(value = 100,message = "年龄最大为100") @RequestParam("age") Integer age) 

Spring Bootspringboot中自定义配置项

文章目录

2. 自定义配置项

在项目开发的过程中,经常需要自定义系统业务方面的配置文件及配置项,Spring Boot提供了@value注解、@ConfigurationProperties注解和Environment接口等3种方式自定义配置项。

@value

在实际项目中,经常需要在配置文件中定义一些简单的配置项,Spring Boot提供@Value注解来设置简单的配置项,默认读取application.properties文件中的配置属性。

我们在application.properties配置文件下自定义配置属性。

然后在使用的位置调用@Value注解来获取配置项的值,如下所示:

@Value("$com.jingt.name.firstName")
    private String firstName;

    @Value("$com.jingt.name.secondName")
    private String secondName;
    
    public String testValue()
        return firstName + "|" + secondName;
    
@Autowired
private HelloServices helloServices;

 @Test
    void testValue() 
        System.out.println(helloServices.testValue());
    

注意:

  1. 使用@Value注解时,所在类必须被Spring容器管理,也就是使用@Component@Controller@Service等注解定义的类。

  2. @Value需要传入完整的配置项的Key值。

  3. @Value注解默认读取application配置文件,如果需要使用其他的配置文件,可以通过@PropertySource注解指定对应的配置文件。

    在启动类上加注解@PropertySource

@SpringBootApplication
@PropertySource(value = "classpath:application_test.properties")
public class HelloworldApplication 
    public static void main(String[] args) 
        SpringApplication.run(HelloworldApplication.class, args);
    

在application_test.properties 配置文件中加入

com.jingt.name.firstName=guo1
com.jingt.name.thirdName=jingt1
@Value("$com.jingt.name.firstName")
private String firstName;

@Value("$com.jingt.name.thirdName")
private String secondName;

public String testValue()
    return firstName + "|" + secondName;

把secondName改为thirdName,firstName从guo改为guo1

会发现thirdName也已经获取到了,firstName还是为guo,这是因为application.properties配置文件加载的优先级的原因,application.properties会覆盖application_test.properties对应文件中的值。

Environment接口

Environment是Spring为运行环境提供的高度抽象的接口,它会自动获取系统加载的全部配置项,包括命令行参数,系统属性,系统环境,随机数,配置文件等。使用时无须其他的额外配置,只要在使用的类中注入Environment即可。

在application.properties文件中增加自定义配置项

com.jingt.name.firstName=guo
com.jingt.name.secondName=jingt

Environment读取的是系统中所有的配置。我们既可以在application.properties中设置自定义的配置项,又可以在自定义配置文件中添加配置项。

Environment对象注入,获取系统配置

   @Autowired
    private Environment env;

    @Test
    void getEnv()
        System.out.println(env.getProperty("com.jingt.name.firstName"));
        System.out.println(env.getProperty("com.jingt.name.secondName"));
    

引入的是import org.springframework.core.env.Environment;,不要引入错了。

使用Environment时还需要注意以下两点:

  1. 使用Environment无须指定配置文件,其获取的是系统加载的全部配置文件中的配置项。
  2. 需要注意配置文件的编码格式,默认为ISO8859-1。

@ConfigurationProperties

在实际项目开发中,需要注入的配置项非常多时,@value和Environment两种方法就会比较烦琐。这时可以使用注解@ConfigurationProperties将配置项和实体Bean关联起来,实现配置项和实体类字段的关联,读取配置文件数据。

在resources下创建自定义的website.properties配置文件,增加配置属性。

com.jingt.resource.name=jingt
com.jingt.resource.website=com.jingt.com
com.jingt.resource.language=java

创建一个配置类

@Configuration
@ConfigurationProperties(prefix = "com.jingt.resource")
@PropertySource(value = "classpath:website.properties")
public class ResourceConfig 
    private String name;
    private String website;
    private String language;

    public String getName() 
        return name;
    

    public void setName(String name) 
        this.name = name;
    

    public String getWebsite() 
        return website;
    

    public void setWebsite(String website) 
        this.website = website;
    

    public String getLanguage() 
        return language;
    

    public void setLanguage(String language) 
        this.language = language;
    

我们使用了@Configuration注解、@ConfigurationProperties和@PropertySource三个注解来定义WebSiteProperties实体类:

  • @Configuration定义此类为配置类,用于构建bean定义并初始化到Spring容器。
  • @ConfigurationProperties(prefix = “com.weiz.resource”)绑定配置项,其中prefix表示所绑定的配置项名的前缀。
  • @PropertySource(value = “classpath:website.properties”)指定读取的配置文件及其路径。@PropertySource不支持引入YML文件。

通过上面的WebSiteProperties类即可读取全部对应的配置项。

在单元测试中,我们测试是否可以通过

@Autowired
    private ResourceConfig config;

    @Test
    void getResourceConfig()
        System.out.println(config.getName());
        System.out.println(config.getWebsite());
        System.out.println(config.getLanguage());
    

使用配置文件注意事项

  1. 使用YML文件时注意空格和格式缩进。
  2. Properties文件默认使用的是ISO8859-1编码格式,容易出现乱码问题。如果含有中文,加入spring.http.encoding.charset=UTF-8配置即可。
  3. Properties配置的优先级高于YML文件。因为YML文件的加载顺序先于Properties文件,如果两个文件存在相同的配置,后面加载的Properties中的配置会覆盖前面YML中的配置。
  4. @PropertySource注解默认只会加载Properties文件,YML文件不能使用此注解。
  5. 简单值推荐使用@Value,复杂对象推荐使用@ConfigurationProperties。
  6. 只有Spring容器中的组件才能使用容器提供的各类方法,所以,配置读取类需要增加@Component注解才能加入Spring容器中。

以上是关于Spring BootSpringBoot参数验证以及实现原理的主要内容,如果未能解决你的问题,请参考以下文章

Spring BootSpringBoot 配置文件详解

Spring BootSpringBoot 配置文件详解

Spring Bootspringboot中自定义配置项

Spring Bootspringboot中自定义配置项

Spring Bootspringboot中自定义配置项

Spring BootSpringBoot中系统多环境配置