我们写代码的过程,事实上就是不断处理异常的过程

if else

  1. if(a.size > 10 && a.size < 100){
  2. Result result = Reuslt.failure("非法参数size , 请检查输入!") ;
  3. return result;
  4. }
  5. if(xxx)
  6. return xxx ;
  7. if (validBusinessData(dataId)) {
  8. Result result = Reuslt.failure("xxx") ;
  9. return result;
  10. }
  11. private boolean validBusinessData(dataId) {
  12. // RPC getByDataId
  13. // 调用结果的处理/判断,balabala...
  14. return Result.isSuccess(data);
  15. }

Bean Validation 2.0 (JSR-380)

  • 最新的Bean验证提议编号为JSR380,提出了Bean Validation 2.0规范
  • Hibernate Validator 6.0.x.Final就是参考Bean Validation 2.0实现的
    • JSR-380规范是bean验证的java api规范,javaEE和javaSE的一部分,使用注解如@NotNull, @Min, and @Max,确保bean属性符合一定条件。
    • JSR-380 需要java8或以上版本,利用java8中新增的特性,如注解类型,支持新的类如:Optional 和 LocalDate。
  • Bean Validation 主页
  • 兼容性表格
Bean Validation Hibernate Validation JDK Spring Boot
1.1 5.4 + 6+ 1.5.x
2.0 6.0 + 8+ 2.0.x

集成

  • 注意:Spring Boot 2.3.0版本后spring-boot-starter-webspring-boot-starter-webflux不再内置validation starter,需手动引入spring-boot-starter-validation
    1. <dependency>
    2. <groupId>org.springframework.boot</groupId>
    3. <artifactId>spring-boot-starter-validation</artifactId>
    4. </dependency>

校验注解

Jakarta Bean Validation/JSR提供的所有校验注解

  • @Null 必须为 null
  • @NotNull 必须不为 null
  • @NotEmpty必须不为 null 或 空
  • @NotBlank字符序列必须不为空,以及去空格后的长度必须大于 0。与@NotEmpty的不同之处在于,此约束只能应用于字符。
  • @AssertTrue 必须为 true
  • @AssertFalse 必须为 false
  • @Digits(integer=, fraction=)必须最多包含 integer 位整数和 fraction 位小数的数字
  • @Email必须为有效的电子邮件地址。可选参数 regexp 和 flags 允许指定电子邮件必须匹配的附加正则表达式(包括正则表达式标志)
  • @Min(value) 必须是一个数值,其值必须大于等于指定的最小值
  • @Max(value) 必须是一个数值,其值必须小于等于指定的最大值
  • @DecimalMin(value)必须是一个数字,其值必须大于等于指定的最小值
  • @DecimalMax(value) 必须是一个数字,其值必须小于等于指定的最大值
  • @Size(max=, min=) 长度必须在指定的范围内(包括),数据类型支持字符CollectionMap数组
  • @Negative必须为负数,不包含零
  • @NegativeOrZero必须为负或零
  • @Positive必须为正数,不包含零
  • @PositiveOrZero必须为正或零
  • @Past 必须是一个过去的日期
  • @Future 必须是一个将来的日期
  • @FutureOrPresent必须是现在或将来的日期
  • @PastOrPresent必须是过去或现在的日期
  • @Pattern(regex=,flag=)必须符合指定的正则表达式

Hibernate Validator提供的常用校验注解

  • @CreditCardNumber(ignoreNonDigitCharacters=)检查是否通过了Luhn校验和测试。此验证旨在检查用户错误,而不是信用卡有效性!ignoreNonDigitCharacters是否忽略非数字字符,默认值为false
  • @Currency(value=)检查货币单位javax.money.MonetaryAmount是否为指定货币单位的一部分
  • @DurationMax(days=, hours=, minutes=, seconds=, millis=, nanos=, inclusive=)检查java.time.Duration元素不大于指定范围。如果将inclusive标识位设置为true,则允许相等
  • @DurationMin(days=, hours=, minutes=, seconds=, millis=, nanos=, inclusive=)检查java.time.Duration元素不少于指定范围。如果将inclusive标识位设置为true,则允许相等
  • @EAN检查字符序列是有效的EAN条形码。类型默认值为EAN-13
  • @ISBN检查字符序列是有效的ISBN国际标准书号。类型默认值为ISBN-13
  • @Length(min=, max=)验证字符序列长度介于(包括)指定的最小值和最大值之间
  • @Range(min=, max=)检查数字值是否介于(包括)指定的最小值和最大值之间
  • @SafeHtml(whitelistType= , additionalTags=, additionalTagsWithAttributes=, baseURI=)检查HTML值是否包含潜在的恶意脚本片段,例如<script/>。通过whitelistType属性,可以选择预定义的白名单类型,可以通过additionalTags或进行细化additionalTagsWithAttributes。前者允许添加没有任何属性的标签,而后者则允许使用注释指定标签和可选的允许属性以及该属性的可接受协议@SafeHtml.Tag。另外baseURI允许指定用于解析相对URI的基本URI
  • @UniqueElements检查集合仅包含唯一元素。使用该equals()方法确定相等性
  • @URL(protocol=, host=, port=, regexp=, flags=)根据RFC2396检查带注释的字符序列是否为有效URL。如果指定任何可选参数protocolhostport时,相应的URL片段必须与指定的值相匹配。可选参数regexpflags允许指定URL必须匹配的其他正则表达式(包括正则表达式标志)

使用示例

校验注解使用示例

  1. public class XBoot {
  2. @Size(max = 16)
  3. @NotNull(message = "不能为空")
  4. private String username;
  5. @Pattern(regexp = "^[1][3,4,5,6,7,8,9][0-9]{9}$", message = "11位手机号格式不正确")
  6. @NotNull(message = "不能为空")
  7. private String mobile;
  8. @Email(message = "邮箱格式不正确")
  9. @NotNull(message = "不能为空")
  10. private String email;
  11. }

@valid 和 @Validated 区别

@Validated:可以用在类型、方法和方法参数上。但是不能用在成员属性(字段)上
@Valid:可以用在方法、构造函数、方法参数和成员属性(字段)上

验证请求参数

  • 验证请求体(RequestBody) ```java @RestController
    @RequestMapping(“/api”)
    publicclass XBootController {

    @PostMapping(“/getPerson”)
    public Result getPerson(@RequestBody @Validated User user) {

    1. return Result.success(user);

    }
    }

  1. - 验证请求参数(Path Variables Request Parameters)
  2. ```java
  3. @RestController
  4. @RequestMapping("/api")
  5. // 注意记得加上该注解
  6. @Validated
  7. public class XBootController {
  8. @GetMapping("/{id}")
  9. public Result<Integer> get(@PathVariable("id") @Validated @Max(value = 999, message = "ID超过指定范围") Integer id,
  10. @RequestParam @Validated @Email(message = "邮箱格式不正确") String email) {
  11. return Result.success(id);
  12. }
  13. }

使用组

  • 某些场景下我们需要对不同类有不同的验证规则,例如新增时不需验证ID(系统生成)而修改时需验证,可使用groups指定验证生效的类
  • 第一步创建两个接口 ```java public interface A { }

public interface B { }

  1. - 第二步指定验证组并使用
  2. ```java
  3. @Null(groups = A.class)
  4. @NotNull(groups = B.class)
  5. private String username;
  1. @Service
  2. @Validated
  3. public class XBootService {
  4. @Validated(A.class)
  5. public void validateA(@Validated XBoot xboot) {
  6. ...
  7. }
  8. @Validated(B.class)
  9. public void validateB(@Validated XBoot xboot) {
  10. ...
  11. }
  12. }

不推荐,易引起歧义

自定义Validator

示例1:校验自定义时间格式如yyyy-MM-dd HH:mm:ss

  1. @Target({FIELD})
  2. @Retention(RUNTIME)
  3. @Documented
  4. @Constraint(validatedBy = {DateValidatorImpl.class})
  5. public @interface DateValidator {
  6. /**
  7. * 必须的属性
  8. */
  9. String message() default "日期字符格式不匹配";
  10. /**
  11. * 必须的属性
  12. * 用于分组校验
  13. */
  14. Class[] groups() default {};
  15. Class[] payload() default {};
  16. /**
  17. * 非必须 接收用户校验的时间格式
  18. */
  19. String dateFormat() default "yyyy-MM-dd HH:mm:ss";
  20. }
  1. import cn.hutool.core.date.DateUtil;
  2. import cn.hutool.core.util.StrUtil;
  3. public class DateValidatorImpl implements ConstraintValidator<DateValidator, String> {
  4. private String dateFormat;
  5. @Override
  6. public void initialize(DateValidator constraintAnnotation) {
  7. // 获取时间格式
  8. this.dateFormat = constraintAnnotation.dateFormat();
  9. }
  10. @Override
  11. public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
  12. if (StrUtil.isBlank(value)) {
  13. return true;
  14. }
  15. try {
  16. Date date = DateUtil.parse(value, dateFormat);
  17. return date != null;
  18. } catch (Exception e) {
  19. return false;
  20. }
  21. }
  22. }

示例2:比较两个属性是否相等

  1. package cn.fxbin.bubble.fireworks.web.validator;
  2. import javax.validation.Constraint;
  3. import javax.validation.Payload;
  4. import javax.validation.constraints.Positive;
  5. import java.lang.annotation.Documented;
  6. import java.lang.annotation.Repeatable;
  7. import java.lang.annotation.Retention;
  8. import java.lang.annotation.Target;
  9. import static java.lang.annotation.ElementType.*;
  10. import static java.lang.annotation.RetentionPolicy.RUNTIME;
  11. /**
  12. * EqualField
  13. *
  14. * <p>
  15. * 比较两个属性是否相等
  16. * </p>
  17. *
  18. * @author fxbin
  19. * @version v1.0
  20. * @since 2020/11/15 12:40
  21. */
  22. @Documented
  23. @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
  24. @Retention(RUNTIME)
  25. @Constraint(validatedBy = {EqualFieldValidator.class})
  26. public @interface EqualField {
  27. /**
  28. * 源属性
  29. */
  30. String source();
  31. /**
  32. * 目标属性
  33. */
  34. String target();
  35. String message() default "both fields must be equal";
  36. Class<?>[] groups() default {};
  37. Class<? extends Payload>[] payload() default {};
  38. }
  1. package cn.fxbin.bubble.fireworks.web.validator;
  2. import lombok.extern.slf4j.Slf4j;
  3. import org.springframework.util.ReflectionUtils;
  4. import javax.validation.ConstraintValidator;
  5. import javax.validation.ConstraintValidatorContext;
  6. import javax.validation.ValidationException;
  7. import java.lang.reflect.Field;
  8. /**
  9. * EqualFieldValidator 相等值校验器
  10. *
  11. * @author fxbin
  12. * @version v1.0
  13. * @since 2020/11/15 12:40
  14. */
  15. @Slf4j
  16. public class EqualFieldValidator implements ConstraintValidator<EqualField, Object> {
  17. private String source;
  18. private String target;
  19. @Override
  20. public void initialize(EqualField constraintAnnotation) {
  21. this.source = constraintAnnotation.source();
  22. this.target = constraintAnnotation.target();
  23. }
  24. /**
  25. * isValid
  26. *
  27. * @author fxbin
  28. * @since 2020/11/15 12:48
  29. * @param object 传入值对象
  30. * @param context 上下文
  31. * @return boolean
  32. */
  33. @Override
  34. public boolean isValid(Object object, ConstraintValidatorContext context) {
  35. Class<?> clazz = object.getClass();
  36. Field srcField = ReflectionUtils.findField(clazz, this.source);
  37. Field dstField = ReflectionUtils.findField(clazz, this.target);
  38. try {
  39. if (srcField == null || dstField == null) {
  40. throw new ValidationException("反射获取变量失败");
  41. }
  42. srcField.setAccessible(true);
  43. dstField.setAccessible(true);
  44. Object src = srcField.get(object);
  45. Object dst = dstField.get(object);
  46. // 其中一个变量为 null 时,则必须两个都为 null 才相等
  47. if (src == null || dst == null) {
  48. return src == dst;
  49. }
  50. // 如果两个对象内存地址相同,则一定相等
  51. if (src == dst) {
  52. return true;
  53. }
  54. // 调用 equals 方法比较
  55. return src.equals(dst);
  56. } catch (Exception e) {
  57. log.warn("EqualFieldValidator 校验异常", e);
  58. return false;
  59. }
  60. }
  61. }

全局异常处理

Springboot 提供了一个 @RestControllerAdvice 注解以及 @ExceptionHandler 注解,前者是用来开启全局的异常捕获,后者则是说明捕获哪些异常,对那些异常进行处理。

  • 结合Sentinel的话,业务异常可以交由 sentinel 记录 Tracer.trace(e); 作为熔断等的重要指标项等…伪代码如下 ```java

@Slf4j @RestControllerAdvice public class DefaultGlobalExceptionHandler {

  1. @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
  2. @ExceptionHandler(value = ServiceException.class)
  3. public Result<String> exceptionHandler(ServiceException ex) {
  4. log.warn("[ServiceException]", ex);
  5. // 交由Sentinel 记录
  6. Tracer.trace(e);
  7. return Result.failure((ex.getErrcode() == 0 ? ResultCode.FAILURE.getCode() : ex.getErrcode()), ex.getMessage());
  8. }

}

  1. - 全局异常处理器,将 Exception 翻译成 Result + 对应的异常编号,默认 -1 标识处理请求失败,0 标识处理成功,这里有一点歧义,参考各大厂商 (阿里云,腾讯等) API, 通常用 0 标识请求处理成功
  2. - Dubbo RPC 调用异常问题, 需要针对默认实现进行定制化修改,避免被Wrap一层RuntimeException 丢失原始异常, v2.5.3 源码如下 `com.alibaba.dubbo.rpc.filter.ExceptionFilter`
  3. ```java
  4. /*
  5. * Copyright 1999-2011 Alibaba Group.
  6. *
  7. * Licensed under the Apache License, Version 2.0 (the "License");
  8. * you may not use this file except in compliance with the License.
  9. * You may obtain a copy of the License at
  10. *
  11. * http://www.apache.org/licenses/LICENSE-2.0
  12. *
  13. * Unless required by applicable law or agreed to in writing, software
  14. * distributed under the License is distributed on an "AS IS" BASIS,
  15. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  16. * See the License for the specific language governing permissions and
  17. * limitations under the License.
  18. */
  19. package com.alibaba.dubbo.rpc.filter;
  20. import java.lang.reflect.Method;
  21. import com.alibaba.dubbo.common.Constants;
  22. import com.alibaba.dubbo.common.extension.Activate;
  23. import com.alibaba.dubbo.common.logger.Logger;
  24. import com.alibaba.dubbo.common.logger.LoggerFactory;
  25. import com.alibaba.dubbo.common.utils.ReflectUtils;
  26. import com.alibaba.dubbo.common.utils.StringUtils;
  27. import com.alibaba.dubbo.rpc.Filter;
  28. import com.alibaba.dubbo.rpc.Invocation;
  29. import com.alibaba.dubbo.rpc.Invoker;
  30. import com.alibaba.dubbo.rpc.Result;
  31. import com.alibaba.dubbo.rpc.RpcContext;
  32. import com.alibaba.dubbo.rpc.RpcException;
  33. import com.alibaba.dubbo.rpc.RpcResult;
  34. import com.alibaba.dubbo.rpc.service.GenericService;
  35. /**
  36. * ExceptionInvokerFilter
  37. * <p>
  38. * 功能:
  39. * <ol>
  40. * <li>不期望的异常打ERROR日志(Provider端)<br>
  41. * 不期望的日志即是,没有的接口上声明的Unchecked异常。
  42. * <li>异常不在API包中,则Wrap一层RuntimeException。<br>
  43. * RPC对于第一层异常会直接序列化传输(Cause异常会String化),避免异常在Client出不能反序列化问题。
  44. * </ol>
  45. *
  46. * @author william.liangf
  47. * @author ding.lid
  48. */
  49. @Activate(group = Constants.PROVIDER)
  50. public class ExceptionFilter implements Filter {
  51. private final Logger logger;
  52. public ExceptionFilter() {
  53. this(LoggerFactory.getLogger(ExceptionFilter.class));
  54. }
  55. public ExceptionFilter(Logger logger) {
  56. this.logger = logger;
  57. }
  58. public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
  59. try {
  60. Result result = invoker.invoke(invocation);
  61. if (result.hasException() && GenericService.class != invoker.getInterface()) {
  62. try {
  63. Throwable exception = result.getException();
  64. // 如果是checked异常,直接抛出
  65. if (! (exception instanceof RuntimeException) && (exception instanceof Exception)) {
  66. return result;
  67. }
  68. // 在方法签名上有声明,直接抛出
  69. try {
  70. Method method = invoker.getInterface().getMethod(invocation.getMethodName(), invocation.getParameterTypes());
  71. Class<?>[] exceptionClassses = method.getExceptionTypes();
  72. for (Class<?> exceptionClass : exceptionClassses) {
  73. if (exception.getClass().equals(exceptionClass)) {
  74. return result;
  75. }
  76. }
  77. } catch (NoSuchMethodException e) {
  78. return result;
  79. }
  80. // 未在方法签名上定义的异常,在服务器端打印ERROR日志
  81. logger.error("Got unchecked and undeclared exception which called by " + RpcContext.getContext().getRemoteHost()
  82. + ". service: " + invoker.getInterface().getName() + ", method: " + invocation.getMethodName()
  83. + ", exception: " + exception.getClass().getName() + ": " + exception.getMessage(), exception);
  84. // 异常类和接口类在同一jar包里,直接抛出
  85. String serviceFile = ReflectUtils.getCodeBase(invoker.getInterface());
  86. String exceptionFile = ReflectUtils.getCodeBase(exception.getClass());
  87. if (serviceFile == null || exceptionFile == null || serviceFile.equals(exceptionFile)){
  88. return result;
  89. }
  90. // 是JDK自带的异常,直接抛出
  91. String className = exception.getClass().getName();
  92. if (className.startsWith("java.") || className.startsWith("javax.")) {
  93. return result;
  94. }
  95. // 是Dubbo本身的异常,直接抛出
  96. if (exception instanceof RpcException) {
  97. return result;
  98. }
  99. // 否则,包装成RuntimeException抛给客户端
  100. return new RpcResult(new RuntimeException(StringUtils.toString(exception)));
  101. } catch (Throwable e) {
  102. logger.warn("Fail to ExceptionFilter when called by " + RpcContext.getContext().getRemoteHost()
  103. + ". service: " + invoker.getInterface().getName() + ", method: " + invocation.getMethodName()
  104. + ", exception: " + e.getClass().getName() + ": " + e.getMessage(), e);
  105. return result;
  106. }
  107. }
  108. return result;
  109. } catch (RuntimeException e) {
  110. logger.error("Got unchecked and undeclared exception which called by " + RpcContext.getContext().getRemoteHost()
  111. + ". service: " + invoker.getInterface().getName() + ", method: " + invocation.getMethodName()
  112. + ", exception: " + e.getClass().getName() + ": " + e.getMessage(), e);
  113. throw e;
  114. }
  115. }
  116. }

逻辑异常

  • 封装统一的业务异常类 ServiceException/BusinessException ,里面有错误码和错误提示,然后进行 throws 抛出。
  • 封装通用的返回类 Result ,里面有错误码和错误提示,然后进行 return 返回。

说一下选择直接返回 Result 的问题:

  1. Spring 是基于 @Transactional 声明式事务进行回滚的,如果采用 Result 的方式,那么事务回滚会很麻烦。PS:关于声明式事务可以查看 org.springframework.transaction.PlatformTransactionManager 接口, 参见下图:

image.png

  1. 调用 其它 方法,如果是 Result ,那么需要对每一次调用进行判断,是否成功处理,很麻烦

基于上诉两点原因,个人认为,采取 异常+全局异常处理的方式更好