程序员必备利器:多语言参数校验的实用指南!

简介: 随着业务的发展碰上了多语言,多区域,原本的参数错误提示语就不太够用了。当 APP 切换到别的区域,比如美国,接口出错提示语还是中文这就不太行了。所以我们今天就要解决它。

前言

  在此之前,写过在两篇文章,是关于如何在 SpringBoot 内实现统一参数校验和自定义校验注解的。毕竟作为后端来讲,对于前端传来的数据,需要保持高度的警惕。避免出现异常数据,导致系统异常。统一参数校验和自定义校验注解,可以帮助我们更加优雅和严格的完成参数校验,减少出错的概率。

    /**
     * 账户名
     */
    @Email(message = "邮箱格式有误")
    @NotBlank(message = "账户名称不能为空")
    @ApiModelProperty(notes = "账户名", required = true)
    private String accountName;

  随着业务的发展碰上了多语言,多区域,原本的参数错误提示语就不太够用了。当 APP 切换到别的区域,比如美国,接口出错提示语还是中文这就不太行了。所以我们今天就要解决它。

准备工作

  我们可以用 idea 初始化一个最基本的项目,然后配置一下统一参数校验。如下图所示:

LoginBo

@Data
public class LoginBo {
   
   

    /**
     * 账户名
     */
    @NotBlank(message = "账户名称不能为空")
    private String accountName;

    /**
     * 密码
     */
    @NotBlank(message = "密码不能为空")
    private String password;

}

ResultVo

public class ResultVo<T> {
   
   

    private String code;
    private String msg;
    private T data;


    public ResultVo() {
   
   
    }

    public ResultVo(String code, String msg) {
   
   
        this(code, msg, null);
    }

    public ResultVo(String code, String msg, T data) {
   
   
        this.code = code;
        this.msg = msg;
        this.data = data;
    }

    // 省略一些方法
}

TestController

@RestController
@RequestMapping("/test")
public class TestController {
   
   

    @PostMapping("/demo")
    public ResultVo<Void> demo(@RequestBody @Validated LoginBo bo) {
   
   
        System.out.println(bo);
        return ResultVo.success();
    }
}

GlobalExceptionHandler

@Component
@RestControllerAdvice
public class GlobalExceptionHandler {
   
   

    /**
     * 参数校验不通过
     *
     * @param e BindException
     * @return ResultVo<Void>
     */
    @ExceptionHandler(BindException.class)
    public ResultVo<Void> handlerBindException(BindException e) {
   
   
        return ResultVo.failure(this.buildMsg(e.getBindingResult()));
    }

    /**
     * 参数校验不通过
     *
     * @param e MethodArgumentNotValidException
     * @return ResultVo<Void>
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResultVo<Void> handlerMethodArgumentNotValidException(MethodArgumentNotValidException e) {
   
   
        return ResultVo.failure(buildMsg(e.getBindingResult()));
    }

    /**
     * 构建参数错误提示信息
     *
     * @param bindingResult BindingResult
     * @return String
     */
    private String buildMsg(BindingResult bindingResult) {
   
   
        StringBuilder builder = new StringBuilder(32);
        for (FieldError error : bindingResult.getFieldErrors()) {
   
   
            builder.append(", [").append(error.getField()).append(":").append(error.getDefaultMessage()).append("]");
        }
        return builder.substring(2);
    }

}

实现方式

  首先明确一下我们的需求点:就是针对不同的语言,接口对应的错误提示语要不一样。这就意味着错误提示语是动态的不能写死。实现思路如下:

  1. 我们可以先针对不同的语言,翻译好对应的错误提示语,并生成相应的配置文件。
  2. 让注解内的 message 指向对应文件内的错误提示语。

配置文件

  这里其实是使用了 Spring Boot 提供的国际化支持来配置多语言提示语。首先,在资源文件中创建多个语言的属性文件,例如 messages.properties 表示默认的英文提示语,messages_zh_CN.properties 表示中文提示语。

  1. 中文配置文件:messages_zh_CN
account.name=账户名称不能为空
password=密码不能为空
  1. 英文配置文件:messages_en_US
account.name=account name cannot be empty
password=password cannot be empty

错误提示语指向配置文件

@Data
public class LoginBo {
   
   

    /**
     * 账户名
     */
    @NotBlank(message = "{account.name}")
    private String accountName;

    /**
     * 密码
     */
    @NotBlank(message = "{password}")
    private String password;

}

演示一下

  理想很丰满,现实很骨感。貌似并没有生效,而是把我们的占位符,直接当提示语输出了。

  不要慌,这个其实是没有指定对应的配置文件,让我们配置一下,先设置为中文的。

@Configuration
public class ValidationConfig {
   
   

    @Bean
    public MessageSource messageSource() {
   
   
        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
        // 先设置中文
        messageSource.setBasename("messages_zh_CN");
        messageSource.setDefaultEncoding("UTF-8");
        return messageSource;
    }

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

再次尝试

  直接就成功了,那改成英文,让我们再试一下。

@Configuration
public class ValidationConfig {
   
   

    @Bean
    public MessageSource messageSource() {
   
   
        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
        messageSource.setBasename("messages_en_US");
        messageSource.setDefaultEncoding("UTF-8");
        return messageSource;
    }

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

改进一下

  通过上面的示例可以看到,虽然是实现了动态,但是还不够优雅。切换的时候,需要修改对应的配置代码。所以让我们改进下,把这部分也做成配置,在启动的时候进行指定就好了,这样方便在部署不同区域的时候可以动态进行配置。

  1. 增加默认语言配置
# 服务端口
server:
  port: 10000

# 配置默认语言
app:
  default:
    language: zh_CN
  1. 读取配置文件的默认语言
@Slf4j
@Configuration
public class ValidationConfig {
   
   

    @Value("${app.default.language}")
    private String defaultLanguage;

    @Bean
    public MessageSource messageSource() {
   
   
        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
        messageSource.setBasename("messages_" + defaultLanguage);
        messageSource.setDefaultEncoding("UTF-8");
        log.info("Message Source init suc -> lang:{}", defaultLanguage);
        return messageSource;
    }

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

  再次测试一下,结果如下:

敲黑板

  虽然上面实现了功能,但是其实是违反了设计原则的,为什么这样说呢?我们可以看看setBasename的注释,看看它是如何使用的。如下图所示:

译文

  从注释总可以发现,basename 需要遵循不指定文件扩展名或语言代码的基本 ResourceBundle 约定。很明显我们违反了。

@Bean
public MessageSource messageSource() {
   
   
    ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
    messageSource.setBasename("messages_en_US");
    messageSource.setDefaultEncoding("UTF-8");
    return messageSource;
}

  所以正确的方式应该是这样的。

@Bean
public MessageSource messageSource() {
   
   
    ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
    messageSource.setBasename("messages");
    messageSource.setDefaultEncoding("UTF-8");
    return messageSource;
}

  这个时候你可能会有疑问。如果不进行指定,那系统咋知道选用哪个配置文件呢?这个问题的答案就是,上面提到的 basename 需要遵循 ResourceBundle 约定。

ResourceBundle

  ResourceBundle 是 Java 标准库中的一个类,用于加载和管理国际化资源。它提供了一种机制来加载不同语言和区域的资源文件,并根据当前的 Locale 进行国际化处理。它提供了以下主要功能:

  1. 选择合适的资源文件:根据给定的 Locale,ResourceBundle 可以选择最匹配的资源文件。如果找不到完全匹配的资源文件,它会尝试找到默认的资源文件或向上回退到更通用的语言环境。
  2. 加载资源文件:ResourceBundle 会负责加载属性文件,并将其缓存在内存中,以便在需要时进行快速访问。
  3. 获取国际化消息:通过资源文件中定义的键,您可以使用 ResourceBundle 获取相应的国际化消息。ResourceBundle 将根据当前的 Locale 自动选择正确的资源文件,并返回与给定键对应的消息。

Locale

  在 Spring Boot 中,默认的 Locale 是根据操作系统的默认语言环境来确定的。它是通过调用 Locale.getDefault() 方法获取的。Locale.getDefault() 方法返回的是 JVM 运行环境的默认 Locale。

  如果您在操作系统中设置了特定的默认语言,那么 Spring Boot 应用程序将使用该默认语言作为默认的 Locale。如果操作系统没有明确设置默认语言,那么它可能会使用 JVM 的默认语言设置。请注意,如果您在 Spring Boot 应用程序中显式设置了其他的 Locale,它将覆盖操作系统的默认设置。

小结一下

  看到这里,我们可以对上面的问题小结一下了。为什么只需要设置 basename 即可?由于 basename 会遵循 ResourceBundle 约定。ResourceBundle 会根据 Spring Boot 获取到 Locale 选择来匹配资源文件。

  并且由于 ResourceBundle 的特点,如果找不到完全匹配的资源文件,它会尝试找到默认的资源文件或向上回退到更通用的语言环境。如果还找不到,那就只能把{xx.xxx}当提示语输出了,也不会影响系统运行。所以最后配置就变成这样了:

@Slf4j
@Configuration
public class ValidationConfig {
   
   


    @Bean
    public MessageSource messageSource() {
   
   
        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
        messageSource.setBasename("messages");
        messageSource.setDefaultEncoding("UTF-8");
        return messageSource;
    }

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

}

  到了这里,貌似已经差不多了。但是有个问题,spring boot 默认是取操作系统的 Locale,如果取不到再取 JVM 的。假如服务器配置的是英文,接口需要返回中文,这不就有问题了吗。毕竟找运维大哥去修改还不如自己通过代码处理。

  处理方式如下:我们可以从配置文件读取默认语言配置,然后生成一个LocaleResolver

@Slf4j
@Configuration
public class ValidationConfig {
   
   

    @Value("${app.default.language}")
    private String defaultLanguage;

    @Bean
    public MessageSource messageSource() {
   
   
        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
        messageSource.setBasename("messages");
        messageSource.setDefaultEncoding("UTF-8");
        return messageSource;
    }

    @Bean
    public LocaleResolver localeResolver() {
   
   
        SessionLocaleResolver resolver = new SessionLocaleResolver();
        resolver.setDefaultLocale(new Locale(defaultLanguage));
        return resolver;
    }

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

}

  再次测试一下,结果如我们所愿:

总结

  该功能的实现主要依托于Spring Boot多语言。实现思路是:预先生成好对应的多语言配置文件,在需要实现多语言的地方跟配置文件进行关联,然后在设置对应Locale即可。

  当前我们只是实现了一个简单的案例。适用的场景是:服务部署在不同的区域,返回对应区域语言的提示语。

  假如我们的需求在进阶一点呢?在同一个区域,需要根据请求头内x-lang的标记语言类型,动态返回呢?并且配置文件不想写死在本地,比如放在nacos或者mysql内实现热更新呢?我们下期继续聊。

结尾

  如果觉得对你有帮助,可以多多评论,多多点赞哦,也可以到我的主页看看,说不定有你喜欢的文章,也可以随手点个关注哦,谢谢。

  我是不一样的科技宅,每天进步一点点,体验不一样的生活。我们下期见!

相关文章
|
25天前
|
中间件 Go API
Go语言中几种流行的Web框架,如Beego、Gin和Echo,分析了它们的特点、性能及适用场景,并讨论了如何根据项目需求、性能要求、团队经验和社区支持等因素选择最合适的框架
本文概述了Go语言中几种流行的Web框架,如Beego、Gin和Echo,分析了它们的特点、性能及适用场景,并讨论了如何根据项目需求、性能要求、团队经验和社区支持等因素选择最合适的框架。
65 1
|
7月前
|
JavaScript Java 测试技术
基于JAVA语言的在线考试与学习交流网页平台附带文章和源代码设计说明文档ppt
基于JAVA语言的在线考试与学习交流网页平台附带文章和源代码设计说明文档ppt
43 1
|
7月前
|
小程序 JavaScript Java
阅读小程序|基于微信阅读网站小程序的系统设计与实现(源码+数据库+文档)
阅读小程序|基于微信阅读网站小程序的系统设计与实现(源码+数据库+文档)
95 0
|
7月前
|
前端开发 JavaScript 小程序
系统刷JavaScripit 构建前端体系(语法篇)
系统刷JavaScripit 构建前端体系(语法篇)
42 1
|
7月前
|
API 开发工具 开发者
全面的开发者文档和用户目标解析:API 文档指南和开发者旅程
开发者文档,也称为 API 文档,是一种专门针对软件开发人员的技术写作形式。这种类型的文档通常包括 API 的技术规范、代码注释、软件设计和架构以及软件开发中涉及的其他详细技术描述。开发者文档是开发人员的重要工具,因为它提供了使用和集成特定软件、库或 API 的必要指南、标准和示例。开发者文档的结构和内容的全面性会根据它所描述的软件的复杂性而大不相同,但主要目的是帮助开发人员理解、使用和高效地为软件做出贡献。
504 2
|
存储 数据可视化 Ubuntu
bcftools学习笔记丨软件简介、安装方式、使用方法、核心功能、参数解释等一文速览
bcftools学习笔记丨软件简介、安装方式、使用方法、核心功能、参数解释等一文速览
|
前端开发 JavaScript 测试技术
前端国际化辅助工具——自动替换中文并翻译
前端国际化辅助工具——自动替换中文并翻译
665 0
|
存储 算法 小程序
小白也能看懂的二维码生成器 API 的技术原理(附Java 接入代码)
二维码生成器 API 是利用是一种通过 Web 服务将文本、链接、图像等信息转化为二维码图像的技术
462 0
小白也能看懂的二维码生成器 API 的技术原理(附Java 接入代码)
|
XML 移动开发 前端开发
这16种原生函数和属性的区别,你真的知道吗? 精心收集,高级前端必备知识,快快打包带走
原生内置了很多API, 作用类似,却也有差千差万别,了解其区别,掌握前端基础,是修炼上层,成为前端高级工程师的必备知识,让我们一起来分类归纳,一起成长吧。
202 0
这16种原生函数和属性的区别,你真的知道吗? 精心收集,高级前端必备知识,快快打包带走
|
Arthas 监控 Java
XPocket插件使用案例合集——性能问题排查分析,一个XPocket足以!
XPocket插件使用案例合集——性能问题排查分析,一个XPocket足以!