在日常的接口开发中,为了防止非法参数对业务造成影响,经常需要对接口的参数进行校验,例如登录的时候需要校验用户名和密码是否为空,添加用户的时候校验用户邮箱地址、手机号码格式是否正确。 靠代码对接口参数一个个校验的话就太繁琐了,代码可读性极差。
进行参数验证是软件开发中的一个重要环节,其主要原因包括但不限于以下几点:
因此,参数验证是构建高质量、安全、易用的应用程序不可或缺的一环。
@PostMapping("/parameterCheck") @Operation(summary = "参数校验", description = "嵌套参数校验-测试") public CommonResult parameterCheck(@RequestBody TestDto dto) { if (dto == null){ throw new RuntimeException("参数不能为空"); } return CommonResult.SUCCESS(dto); }
缺点:
因此,在设计代码时,推荐采用诸如策略模式、状态模式等设计模式来替代复杂的if判断,或者使用Switch语句(在适用的情况下)来提高代码的清晰度和可维护性。同时,也可以考虑利用函数式编程的思想,将逻辑分解为更小的、可重用的函数,以提高代码的模块化程度。
@PostMapping("/parameterCheck") @Operation(summary = "参数校验", description = "嵌套参数校验-测试") public CommonResult parameterCheck(@RequestBody TestDto dto) { Assert.isNull(dto.getName(), "姓名不能为空"); Assert.isNull(dto.getSex(), "性别不能为空"); return CommonResult.SUCCESS(dto); }
使用Assert
语句进行参数校验在Java等编程语言中较为常见,尤其是在单元测试中用于验证预期结果。然而,在生产代码中过度依赖Assert
进行参数校验也存在一些缺点:
Assert
主要用于开发阶段的自我检查,它抛出的是AssertionError
,这是一种错误而非异常。在默认的Java虚拟机设置下,生产环境通常不启用断言(即-ea
标志未设置),这意味着断言不会执行,从而无法起到参数校验的作用。AssertionError
通常是直接终止程序的,没有被捕获和处理的机制,这会导致程序突然崩溃,给用户带来不友好的体验。Assert
主要用于验证程序内部不变性条件,其信息更多服务于开发者调试,而不能提供丰富的错误信息反馈或自定义错误处理逻辑。Assert
在生产环境中默认不启用,可能导致某些错误在开发阶段未被发现,而在生产环境中因为不同的配置导致问题浮现,增加了问题排查的难度。Assert
进行参数校验并不合适,因为它缺乏控制异常流和提供恢复机制的能力。Validator
框架就是为了解决开发人员在开发的时候少写代码,提升开发效率;Validator专门用来进行接口参数校验,例如常见的必填校验,email格式校验,用户名必须位于6到12之间等等。
注意:如果spring-boot版本小于2.3.x,spring-boot-starter-web会自动传入hibernate-validator依赖。如果spring-boot版本大于2.3.x,则需要手动引入依赖。我这里使用的SpringBoot版本是3.0.0,因此手动引入了。
org.springframework.boot spring-boot-starter-validation
常见的约束注解如下:
注解 | 功能 |
---|---|
@AssertFalse | 可以为null,如果不为null的话必须为false |
@AssertTrue | 可以为null,如果不为null的话必须为true |
@DecimalMax | 设置不能超过最大值 |
@DecimalMin | 设置不能超过最小值 |
@Digits | 设置必须是数字且数字整数的位数和小数的位数必须在指定范围内 |
@Future | 日期必须在当前日期的未来 |
@Past | 日期必须在当前日期的过去 |
@Max | 最大不得超过此最大值 |
@Min | 最大不得小于此最小值 |
@NotNull | 不能为null,可以是空 |
@Null | 必须为null |
@Pattern | 必须满足指定的正则表达式 |
@Size | 集合、数组、map等的size()值必须在指定范围内 |
必须是email格式 | |
@Length | 长度必须在指定范围内 |
@NotBlank | 字符串不能为null,字符串trim()后也不能等于"" |
@NotEmpty | 不能为null,集合、数组、map等size()不能为0;字符串trim()后可以等于"" |
@Range | 值必须在指定范围内 |
@URL | 必须是一个URL |
import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.*; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @AllArgsConstructor @NoArgsConstructor public class TestDto { @NotBlank(message = "姓名不能为空") @Schema(description = "姓名") private String name; @NotNull(message = "年龄不能为空") @Schema(description = "年龄") @Min(value = 0, message = "年龄不能小于0") @Max(value = 200, message = "年龄不能大于200") private Integer age; //性别只允许为男或女 @NotBlank(message = "性别不能为空") @Pattern(regexp = "^(男|女)$", message = "性别必须为'男'或'女'") @Schema(description = "性别") private String sex; @Valid @Schema(description = "嵌套对象") private TestDtoObj testDtoObj; }
import com.example.demo.annotation.PhoneNumber; import com.fasterxml.jackson.annotation.JsonFormat; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.*; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import org.hibernate.validator.constraints.URL; import java.time.LocalDate; import java.time.LocalDateTime; @Data @AllArgsConstructor @NoArgsConstructor public class TestDtoObj { @PhoneNumber @NotBlank(message = "手机号1不能为空") @Schema(description = "手机号1") private String phone1; @Pattern(regexp = "^1[3-9]\\d{9}$", message = "无效的手机号码格式") @NotBlank(message = "手机号不能为空") @Schema(description = "手机号2") private String phone2; @NotBlank(message = "密码不能为空") @Size(min = 6, max = 16, message = "密码长度必须在6到16个字符之间") @Schema(description = "密码") private String password; @NotBlank(message = "邮箱不能为空") @Pattern(regexp = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", message = "邮箱格式不正确") @Schema(description = "邮箱") private String email; @Digits(integer = 4, fraction = 2, message = "整数位数必须在4位以内小数位数必须在2位以内") @Schema(description = "小数") private Double num; @URL(message = "url格式错误") @Schema(description = "地址") private String url; @Past(message = "日期必须为过去日期") @Schema(description = "过去日期") private LocalDate pastDate; @Future(message = "日期必须为将来日期") @Schema(description = "将来日期") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8") private LocalDateTime futureDate; }
Validator
框架 抛出的特定异常为MethodArgumentNotValidException,该异常会将我们在参数校验注解自定义的message返回到e.getBindingResult().getFieldError().getDefaultMessage()中。
/** * 全局异常拦截 * * @author zyw */ @Slf4j @RestControllerAdvice public class BaseExceptionHandler { /** * 拦截参数校验异常 * @param e * @param request * @return */ @ExceptionHandler(MethodArgumentNotValidException.class) public CommonResult> handleGlobalException(MethodArgumentNotValidException e, HttpServletRequest request) { log.error("请求地址'{}',发生系统异常'{}'", request.getRequestURI(), e.getBindingResult().getFieldError().getDefaultMessage()); return CommonResult.ECEPTION(ResultCode.PARAMETER_EXCEPTION, e.getBindingResult().getFieldError().getDefaultMessage()); } }
import com.example.demo.config.CommonResult; import com.example.demo.model.dto.TestDto; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.enums.ParameterIn; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.extern.slf4j.Slf4j; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; @RestController @Slf4j @RequestMapping("knife4j") @Tag(name = "knife4j测试控制器") public class Knife4jController { @PostMapping("/parameterCheck") @Operation(summary = "参数校验", description = "嵌套参数校验-测试") public CommonResult parameterCheck(@Validated @RequestBody TestDto dto) { return CommonResult.SUCCESS(dto); } }
在Java项目中,自定义注解是一种强大的功能,允许开发者创建自己的注解类型来满足特定需求,比如验证、日志记录、性能监控等。我们通过自定义注解修饰特定的接口、方法、属性、类,可以实现更加灵活的功能。
import com.example.demo.uitls.validator.PhoneNumberValidator; import jakarta.validation.Constraint; import jakarta.validation.Payload; import java.lang.annotation.*; /** * PhoneNumber : 手机号格式验证注解 * 用于验证电话号码格式的注解。 * 该注解可以应用于字段或参数上,以验证其是否为有效的电话号码格式。 * 默认的错误消息是“无效的手机号码格式”,但可以通过message属性自定义。 * 可以通过groups和payload属性来支持分组验证和负载信息。 * * @Documented 标记此注解将被包含在文档中。 * @Constraint 标记此注解为约束注解,并指定PhoneNumberValidator类作为验证器。 * @Target 指定此注解可以应用于字段和参数上。 * @Retention 指定此注解在运行时保留。 * @author zyw * @create 2024-05-31 15:38 */ @Documented @Constraint(validatedBy = PhoneNumberValidator.class) @Target({ElementType.FIELD, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) public @interface PhoneNumber { /** * 验证失败时的错误消息,默认为“无效的手机号码格式”。 * 可以通过将此属性设置为自定义错误消息来更改默认消息。 * * @return 验证失败时的错误消息。 */ String message() default "无效的手机号码格式"; /** * 定义验证的分组,默认为空组。 * 可以通过将此属性设置为一个或多个分组类来指定字段应在哪些分组中进行验证。 * * @return 验证的分组类数组。 */ Class>[] groups() default {}; /** * 定义验证的负载信息,默认为空负载。 * 可以通过将此属性设置为一个或多个负载类来携带额外的验证信息。 * * @return 验证的负载信息类数组。 */ Class extends Payload>[] payload() default {}; }
import com.example.demo.annotation.PhoneNumber; import jakarta.validation.ConstraintValidator; import jakarta.validation.ConstraintValidatorContext; /** * PhoneNumberValidator : 手机号验证器类 * * @author zyw * @create 2024-05-31 15:39 */ public class PhoneNumberValidator implements ConstraintValidator { private static final String PHONE_PATTERN = "^1[3-9]\\d{9}$"; // 中国手机号码的简单正则表达式 @Override public boolean isValid(String phoneNumber, ConstraintValidatorContext context) { return phoneNumber != null && phoneNumber.matches(PHONE_PATTERN); } }
import com.example.demo.annotation.PhoneNumber; import com.fasterxml.jackson.annotation.JsonFormat; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.*; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import org.hibernate.validator.constraints.URL; import java.time.LocalDate; import java.time.LocalDateTime; /** * TestDtoObj : * * @author zyw * @create 2024-05-31 15:47 */ @Data @AllArgsConstructor @NoArgsConstructor public class TestDtoObj { @PhoneNumber @PhoneNumber(message = "手机号格式错误") @Schema(description = "手机号1") private String phone1; @Pattern(regexp = "^1[3-9]\\d{9}$", message = "无效的手机号码格式") @NotBlank(message = "手机号不能为空") @Schema(description = "手机号2") private String phone2; }
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class TestDtoObj {
@PhoneNumber @PhoneNumber(message = "手机号格式错误") @Schema(description = "手机号1") private String phone1; @Pattern(regexp = "^1[3-9]\\d{9}$", message = "无效的手机号码格式") @NotBlank(message = "手机号不能为空") @Schema(description = "手机号2") private String phone2;
}