一文教你实现springboot中的自定义validator和错误信息国际化配置

     2022-04-03     697

关键词:

一个在阿里云打工的清华学渣!

本文通过示例说明,在 Springboot 中如何自定义 Validator,以及如何实现国际化的错误信息返回。注意,本文代码千万别直接照抄,有可能会出大事情的。先留个悬念,读者朋友们能从中看出有什么问题吗?

项目初始化入
直接从 springboot 官网中下载模板,直接通过示例中的 GreetingController 添加实现逻辑。

@RestController
public class GreetingController {

  private static final String template = "Hello, %s!";
  private final AtomicLong counter = new AtomicLong();

  @RequestMapping("/greeting")
  public Response<Greeting> greeting(@RequestParam(value = "name", defaultValue = "World") String name) {
    if (!"tangleithu".equals(name)) {
      throw new BadRequestException("user.notFound");
    }
    return Response.ok(new Greeting(counter.incrementAndGet(), String.format(template, name)));
  }
}

以上代码直接源自官方 spring-guides 的 demo,我稍微改吧改吧。正常情况下,能返回正确的结果:

# curl "localhost:8080/greeting?name=tangleithu&lang=en" 

{
    "code": 0,
    "data": {
        "content": "Hello, tangleithu!",
        "id": 9
    },
    "message": "success"
}

国际化需求
作为高大上的项目,我们肯定有海外用户,所以就需要国际化的配置。现在来模拟了下业务逻辑,假设输入的参数有一些校验功能,比如以上name参数,假设和“tangleithu”不相等,就直接返回错误。同时希望返回的错误信息需要实现国际化,即在不同的语言环境下返回的结果不一样。例如中文:“没找到用户呢。” 对应的英文:“User does not exist.”,而对应的德文是……,算了忽略,我也不会。
技术图片
用一个图来表达,即希望实现的效果是,不同国家和地区的用户(不同语言)在遇到同一个业务场景下同一个错误原因,有不同的翻译。例如在参数校验没通过,Http Status Code应该返回 400,并告知错误原因;在具体的 Service 实现时可能也会遇到其他的 case 需要返回某种具体错误信息。利用这种方式就可以很方便地统一管理起来。
注意:实际业务场景中后端可能仅仅只返回错误码,具体的展示由前端根据 key 进行翻译。不过在一些更加灵活的场景中(例如有的 app 实现方案),错误信息很有可能会由后端接口直接返回。本文只是用了一个简单的案例阐述整个流程。

统一错误处理
我们借助 Spring 中的 AOP,用一个 ControllerAdvice 统一拦截这种BadRequestException异常。其他 Exception 也一样,做到异常信息统一处理,也不容易出现安全风险(之前有遇到过某大型网站因为后台发生异常,直接将具体的 SQL 错误暴露出来了,其中还不乏有表结构等敏感信息)。例如:

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(BadRequestException.class)
    @ResponseBody
    public ResponseEntity handle(HttpServletRequest request, BadRequestException e){
        String i18message = getI18nMessage(e.getKey(), request);
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(Response.error(e.getCode(), i18message));
    }
}

国际化配置
具体的错误信息翻译就直接配置在对应语言的资源文件中即可。我们可以在这种具体的异常信息时,用一个 key 来标记错误码,在资源文件中用不同的语言来定义应该返回的具体错误信息。例如本文示例中,添加了中英文两种。相应的目录结构如下:
技术图片
此时,我们只需要在 GlobalExceptionHandler 中根据请求来源是中文还是英文返回对应的错误信息即可。

private String getI18nMessage(String key, HttpServletRequest request) {
   try {
       return messageSource.getMessage(key, null, LanguaggeUtils.currentLocale(request));
   } catch (Exception e) {
       // log
       return key;
   }
}

从请求来源获取语言信息就有多种方式啦,例如我们可以从请求头中获取 Accept-Lanuage,一般浏览器会根据用户的设置情况带上这个请求头的,如下图所示。
技术图片
或者我们自己显示定义一些例如 lang 之类的参数。本文不做详细阐述,咱们就简单用 lang 这个参数来定义,如下:

public class LanguaggeUtils {
    public static Locale currentLocale(HttpServletRequest request) {
        // 从 RequestHeader 等等获取相应的语言信息
        // 简单起见,直接从 queryParams 中取, 只模拟中英文
        String locale = request.getParameter("lang");
        if ("zh".equalsIgnoreCase(locale)) {
            return Locale.CHINA;
        } else {
            return Locale.ENGLISH;
        }
    }
}

这样,通过简单几行代码就能实现高大上的“国际化”参数返回了。试试效果如下:

#curl "localhost:8080/greeting?name=tanglei&lang=en" 
{
    "code": 400,
    "data": null,
    "message": "User does not exist."
}

#curl "localhost:8080/greeting?name=tanglei&lang=zh" 
{
    "code": 400,
    "data": null,
    "message": "没找到用户呢。"
}

Bean Validator
其实针对类似 Form 等参数校验,我们有更简单的方法。那就是借助 SpringBoot 中自带的 Validation 框架,本文用到的这个版本对应的实现是jakarta.validation-api。其实 Bean Validation 都有相应的标准,可能有不同的具体实现而已。对标准感兴趣的可以戳这里 JSR #380 Bean Validation 2.0。

回到本文的 demo 中,假设在我们业务逻辑中需要传递一个 UserForm,接收 age,name,param 三个参数。并对其中输入进行进行校验,其中,param 没有具体的含义,只是为了说明问题。
public class UserForm {
@Min(value = 0, message = "validate.userform.age")
@Max(value = 120, message = "validate.userform.age")
private int age;

@NotNull(message = "validate.userform.name.notEmpty")
private String name;

@CustomParam(message = "validate.userform.param.custom")
private String param;
...

}

@RequestMapping("/user")
public Response<Greeting> createUser(@Valid @RequestBody UserForm userForm) {
return Response.ok(new Greeting(counter.incrementAndGet(), String.format(template, userForm.getName())));
}

代码如上,上面示例只用了很简单的 @Min, @Max, @NotNull等约束条件,通过名字就能看出来含义。更多约束规则可以直接看对应源码 javax.validation.constraints.xxx,比如有常见的 Email 等格式校验。
默认情况下,违反相应的约束条件后,默认的输出比较啰嗦,例如用这个请求 curl -H "Content-Type: application/json" -d "{}" "localhost:8080/user",对应的输出如下:

{
    "error": "Bad Request",
    "errors": [
        {
            "arguments": [
                {
                    "arguments": null,
                    "code": "name",
                    "codes": [
                        "userForm.name",
                        "name"
                    ],
                    "defaultMessage": "name"
                }
            ],
            "bindingFailure": false,
            "code": "NotBlank",
            "codes": [
                "NotBlank.userForm.name",
                "NotBlank.name",
                "NotBlank.java.lang.String",
                "NotBlank"
            ],
            "defaultMessage": "must not be blank",
            "field": "name",
            "objectName": "userForm",
            "rejectedValue": null
        }
    ],
    "message": "Validation failed for object=‘userForm‘. Error count: 1",
    "path": "/user",
    "status": 400,
    "timestamp": "2020-05-10T08:44:12.952+0000"
}

咱们依葫芦画瓢,debug 的时候,把抛出的具体异常添加到前面的 GlobalExceptionHandler,再修改下默认的行为即可。

@ExceptionHandler(BindException.class)
@ResponseBody
public ResponseEntity handle(HttpServletRequest request, BindException e){
   String key = e.getBindingResult().getAllErrors().get(0).getDefaultMessage();
   String i18message = getI18nMessage(key, request);
   return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(Response.error(400, i18message));
}

@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseBody
public ResponseEntity handle(HttpServletRequest request, MethodArgumentNotValidException e){
   String key = e.getBindingResult().getAllErrors().get(0).getDefaultMessage();
   String i18message = getI18nMessage(key, request);
   return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(Response.error(400, i18message));
}

@ExceptionHandler(ConstraintViolationException.class)
@ResponseBody
public ResponseEntity handle(HttpServletRequest request, ConstraintViolationException e){
   String key = e.getConstraintViolations().iterator().next().getMessage();
   String i18message = getI18nMessage(key, request);
   return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(Response.error(400, i18message));
}

改进后,增加自定义的 handler 后,返回信息结构一致方便前端统一处理,同时也简洁不少:

{
    "code": 400,
    "data": null,
    "message": "validate.userform.name.notEmpty"
}

再结合前面讲解的通过i18n的参数配置,又可以实现当没通过校验的时候,错误信息统一由对应的国际化资源文件进行配置了。

自定义 Validator
当内置的满足不了条件的时候,我们希望实现自定义的 Validator,例如前文中的 CustomParam。怎么做呢?我们需要一个 Annotation,方便在对应 Form 的时候引用校验,具体实现如下:

/**
 * @author tanglei
 * @date 2020/5/10
 */
@Documented
@Constraint(validatedBy = CustomValidator.class)
@Target({FIELD, METHOD, PARAMETER, ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomParam {
    String message() default "name.tanglei.www.validator.CustomArray.defaultMessage";

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

    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target({FIELD, METHOD, PARAMETER, ANNOTATION_TYPE})
    @interface List {
        CustomParam[] value();
    }
}

还需要一个具体的 validator 实现类,通过上面的 @Constraint(validatedBy = CustomValidator.class) 关联起来。本文只是 demo,所以具体参数校验没有实际逻辑意义的,下面假设输入的参数和“tanglei”相同则校验通过,否则提示用户输入错误。

public class CustomValidator implements ConstraintValidator<CustomParam, String> {
    @Override
    public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
        if (null == s || s.isEmpty()) {
            return true;
        }
        if (s.equals("tanglei")) {
            return true;
        } else {
            error(constraintValidatorContext, "Invalid params: " + s);
            return false;
        }
    }

    @Override
    public void initialize(CustomParam constraintAnnotation) {
    }

    private static void error(ConstraintValidatorContext context, String message) {
        context.disableDefaultConstraintViolation();
        context.buildConstraintViolationWithTemplate(message).addConstraintViolation();
    }
}

看看效果,输入没校验通过,还提示你输入的参数 “xx” 不符合条件。

是不是感觉很完美?
注意:上文中有一个比较隐蔽的安全漏洞,请大家注意。
注意:上文中有一个比较隐蔽的安全漏洞,请大家注意。
注意:上文中有一个比较隐蔽的安全漏洞,请大家注意。

重要的事情说三遍,总体来说本文思路还是值得借鉴的(对应代码见 github),但一定要注意不要完全照抄,上面说的这个安全漏洞还挺严重的。给予点提示,就是在CustomValidator 的具体实现中,有朋友了解吗?欢迎留言讨论。


一个由跨平台产生的浮点数bug | 有你意想不到的结果。


RSA算法及一种"旁门左道"的***方式。


震惊! 阿里的程序员也不过如此,竟被一个简单的 SQL 查询难住。


面了 7轮Google,最终还是逃不脱被挂的命运。

欢迎关注交流
程序猿石头
技术图片

码农唐磊
有收获 ? 请四连 : 默默点赞
喜欢作者

springboot 微服务中的自定义 JWT 令牌

】springboot微服务中的自定义JWT令牌【英文标题】:CustomJWTtokeninspringbootmicroservices【发布时间】:2022-01-1100:57:23【问题描述】:我们有一个应用程序在成功验证后从外部系统加载信息(特定于用户),为了避免每次api调用到外部... 查看详情

Springboot jpa:实体无法绑定不在表列中的自定义查询中的数据

】Springbootjpa:实体无法绑定不在表列中的自定义查询中的数据【英文标题】:Springbootjpa:Entitycan\'tbinddatafromcustomquerythat\'snotintablecolumn【发布时间】:2021-12-1310:51:22【问题描述】:我使用javaSpringbootJPA,mysql。我需要从我的自定义... 查看详情

Spring Boot 中的自定义异常

】SpringBoot中的自定义异常【英文标题】:CustomExceptioninSprinBoot【发布时间】:2019-12-1209:54:06【问题描述】:我在SPRINGBOOT中编写了以下自定义错误处理程序@RestControllerAdvicepublicclassCustomGlobalExceptionHandlerextendsResponseEntityExceptionHandler@... 查看详情

SpringBoot jars 作为 JBoss EAP 7.0 中的自定义模块

】SpringBootjars作为JBossEAP7.0中的自定义模块【英文标题】:SpringBootjarsasacustommoduleinJBossEAP7.0【发布时间】:2021-02-1820:59:56【问题描述】:我想为SpringBootjar创建一个JBossEAP自定义模块,并在JBoss中的不同战争部署中使用这个自定义模... 查看详情

springboot的自定义配置

参考技术ASpringBoot免除了项目中大部分的手动配置,对一些特定情况,我们可以通过修改全局配置文件以适应具体生产环境,可以说,几乎所有的配置都可以写在application.properties文件中,SpringBoot会自动加载全局配置文件,从而... 查看详情

带有分页的 Spring JPA 存储库中的自定义查询

...布时间】:2015-10-2916:53:32【问题描述】:我已经尝试使用SpringBoot实现JPA存储库,它工作正常。现在,如果我尝试在使用@QueryAnnotation扩展JpaRepository的接口中实现自定义查询,它可以正常返回bean列表。(使用NamedQu 查看详情

@RepositoryRestResource 中的自定义实现

】@RepositoryRestResource中的自定义实现【英文标题】:Customimplementationin@RepositoryRestResource【发布时间】:2020-04-1220:47:11【问题描述】:我正在开发一个使用@RepositoryRestResource的spring-boot项目。有2个实体,Product和Domain,它们具有多对... 查看详情

Spring boot - Application.properties 中的自定义变量

】Springboot-Application.properties中的自定义变量【英文标题】:Springboot-customvariablesinApplication.properties【发布时间】:2015-11-1014:05:59【问题描述】:我有一个使用restfulapi的springboot客户端。我可以使用application.properties中的任何密钥条... 查看详情

springboot验证表单数据并实现数据的自定义验证

参考技术A这里主要用的是hibernate.validator这个内置校验器,这个校验器看了下,能满足大多需求,新需求就自己开发->@MyConstraint这个验证的简单开发实例2.自定义验证业务逻辑的实现类:实现ConstraintValidator这个接口,重写isValid... 查看详情

我的自定义存储库实现中的 CrudRepository

】我的自定义存储库实现中的CrudRepository【英文标题】:CrudRepositoryinsidemycustomrepositoryimplementation【发布时间】:2013-02-1400:26:27【问题描述】:我正在尝试获取对我的存储库接口(UserRepository)的引用,该接口在我的自定义实现(UserRep... 查看详情

手把手教你利用springboot实现各种参数校验

前言本文会详细介绍SpringValidation各种场景下的最佳实践及其实现原理,死磕到底!一键获取源码地址简单使用JavaAPI规范(JSR303)定义了Bean校验的标准validation-api,但没有提供实现。hibernatevalidation是对这个规范的实现... 查看详情

springboot的自定义注解功能实现类该怎么写?

我想做一个类属性上的注解,加了后会修改这个类里的方法,实现这个注解功能的类该怎么写?或者该继承什么接口?这个应该和lombok包的@Getter和@Setter的功能实现类一样的把,我在网上收到过用AOP切面实现的但是这是在方法上... 查看详情

Spring Boot - 实体中的自定义类字段

】SpringBoot-实体中的自定义类字段【英文标题】:SpringBoot-customclassfieldinentity【发布时间】:2017-11-2620:15:20【问题描述】:我有2个自定义类,OzBakim和GunlukEtkinlik。这些类不是实体。我需要在实体中使用这些类。但我得到一个错误... 查看详情

一文教你实现「飞机大战」里战机的控制逻辑(代码片段)

?纵版射击游戏是一种比较经典的游戏类型,从早期的红白机平台到如今的手机平台,一直都有非常经典的游戏作品。纵版射击游戏只需要控制飞行器躲避敌机和子弹并攻击敌机,玩法和操作都非常简单,因此很适合移动平台上... 查看详情

ExtJS 4 中的自定义溢出处理程序实现

】ExtJS4中的自定义溢出处理程序实现【英文标题】:CustomoverflowHandlerimplementationinExtJS4【发布时间】:2012-09-2914:47:26【问题描述】:我有两个toolbars的面板。如何实现自定义类以用作overflowHandler,它将组件移动到第一个工具栏溢出... 查看详情

iOS 中的自定义 Cover Flow 实现

】iOS中的自定义CoverFlow实现【英文标题】:CustomCoverFlowImplementationiniOS【发布时间】:2012-05-0410:36:40【问题描述】:我希望在scrollView中有一个列表按钮,而不是封面流中的图像(对于封面流中的每个图像),单击它们应该导航到... 查看详情

springboot中普通工具类不能使用@value注入yml文件中的自定义参数的问题

在写一个工具类的时候,因为要用到yml中的自定义参数,使用@Value发现值不能正常注入,都显示为null;yml文件中的自定义格式调用工具类的时候不能new的方式要使用@Autowired的方式注入进来,new会导致部分环境未加载,尽可能舍弃... 查看详情

Spring Boot,MongoDB,Pageable,按对象中的自定义方法排序

】SpringBoot,MongoDB,Pageable,按对象中的自定义方法排序【英文标题】:SpringBoot,MongoDB,Pageable,sortbyacustommethodintheobject【发布时间】:2020-04-2214:27:46【问题描述】:比如说我有以下设置,这样的模型:publicclassPost@IdprivateStringid;privat... 查看详情