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

简介: 随着业务的发展碰上了多语言,多区域,原本的参数错误提示语就不太够用了。当 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内实现热更新呢?我们下期继续聊。

结尾

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

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

相关文章
|
9月前
|
测试技术 UED
测试方案有点难?ChatGPT助你轻松编写测试方案!
使用ChatGPT辅助编写测试方案,以高级搜索功能为例,涉及关键词搜索、过滤条件、界面兼容性、错误处理等测试点。首先明确测试需求,如按作者、时间范围和分类搜索,产品形态为App和Web应用。然后,通过提示词逐步细化方案,包括App的移动设备适配、耗电量和内存占用测试。通过不断优化提示词,确保测试方案全面覆盖功能性能、用户体验及专项测试内容。
|
6月前
|
自然语言处理 前端开发 JavaScript
前端进阶必读:JS闭包深度解析,掌握这一特性,你的代码将焕然一新!
【8月更文挑战第23天】闭包是JavaScript的一项高级功能,让函数能够访问和操作外部函数作用域中的变量。本文深入解析闭包概念、组成及应用场景。闭包由函数及其词法环境构成,通过在一个函数内定义另一个函数来创建。它有助于封装私有变量、维持状态和动态生成函数。然而,不当使用闭包可能导致内存泄漏或性能问题。掌握闭包对于实现模块化代码和成为优秀前端开发者至关重要。
52 0
|
9月前
|
前端开发 JavaScript 小程序
系统刷JavaScripit 构建前端体系(语法篇)
系统刷JavaScripit 构建前端体系(语法篇)
|
存储 数据可视化 Ubuntu
bcftools学习笔记丨软件简介、安装方式、使用方法、核心功能、参数解释等一文速览
bcftools学习笔记丨软件简介、安装方式、使用方法、核心功能、参数解释等一文速览
|
机器学习/深度学习 数据管理 Go
量化交易系统开发逻辑策略编写 | 量化交易系统开发源码示例(go语言版)
一个基本的量化交易系统大致上有两个分层:资金管理层与 ( 商品 策略 ) 层。 框架 ( 模组 ) 决定好了,再更有系统地强化各个模组,进而更接近交易本质。 初阶的交易者多数先选定一个邻近市场,如外汇或是熟悉的台股、台指期、台指选择权,并进行策略的开发。 一个基本策略的框架大致上如下,可以分成数个模组,设计者可以依循这样的框架进行一个初步策略开发或交易程式撰写:
|
前端开发
《《前端智能化实践》——逻辑代码生成》电子版地址
《前端智能化实践》——逻辑代码生成--甄子
95 0
《《前端智能化实践》——逻辑代码生成》电子版地址
|
NoSQL 关系型数据库 MySQL
架构进阶-优化邮件发送代码
架构进阶-优化邮件发送代码
232 0
架构进阶-优化邮件发送代码
|
XML 移动开发 前端开发
这16种原生函数和属性的区别,你真的知道吗? 精心收集,高级前端必备知识,快快打包带走
原生内置了很多API, 作用类似,却也有差千差万别,了解其区别,掌握前端基础,是修炼上层,成为前端高级工程师的必备知识,让我们一起来分类归纳,一起成长吧。
219 0
这16种原生函数和属性的区别,你真的知道吗? 精心收集,高级前端必备知识,快快打包带走
|
JavaScript 前端开发 开发工具