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

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

结尾

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

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

相关文章
|
6月前
|
运维 Java
Java版HIS系统 云HIS系统 云HIS源码 结构简洁、代码规范易阅读
云HIS系统分为两个大的系统,一个是基层卫生健康云综合管理系统,另一个是基层卫生健康云业务系统。基层卫生健康云综合管理系统由运营商、开发商和监管机构使用,用来进行运营管理、运维管理和综合监管。基层卫生健康云业务系统由基层医院使用,用来支撑医院各类业务运转。
92 5
|
6月前
|
小程序 开发者
4月开发者日回顾丨小程序开发常见问题解析
4月开发者日回顾丨小程序开发常见问题解析
82 0
|
11天前
|
jenkins Java 测试技术
如何使用 Jenkins 自动发布 Java 代码,通过一个电商公司后端服务的实际案例详细说明
本文介绍了如何使用 Jenkins 自动发布 Java 代码,通过一个电商公司后端服务的实际案例,详细说明了从 Jenkins 安装配置到自动构建、测试和部署的全流程。文中还提供了一个 Jenkinsfile 示例,并分享了实践经验,强调了版本控制、自动化测试等关键点的重要性。
40 3
可控细节的长文档摘要,探索开源LLM工具与实践
本文通过将文档分为几部分来解决这个问题,然后分段生成摘要。在对大语言模型进行多次查询后,可以重建完整的摘要。通过控制文本块的数量及其大小,我们最终可以控制输出中的细节级别。
|
2月前
|
存储 前端开发 API
告别繁琐,拥抱简洁!Python RESTful API 设计实战,让 API 调用如丝般顺滑!
在 Web 开发的旅程中,设计一个高效、简洁且易于使用的 RESTful API 是至关重要的。今天,我想和大家分享一次我在 Python 中进行 RESTful API 设计的实战经历,希望能给大家带来一些启发。
36 3
|
3月前
|
JSON 数据库 开发者
FastAPI入门指南:Python开发者必看——从零基础到精通,掌握FastAPI的全栈式Web开发流程,解锁高效编码的秘密!
【8月更文挑战第31天】在当今的Web开发领域,FastAPI迅速成为开发者的热门选择。本指南带领Python开发者快速入门FastAPI,涵盖环境搭建、基础代码、路径参数、请求体处理、数据库操作及异常处理等内容,帮助你轻松掌握这一高效Web框架。通过实践操作,你将学会构建高性能的Web应用,并为后续复杂项目打下坚实基础。
105 0
|
3月前
|
开发者 存储 API
Xamarin 开发者的社区资源概览:从官方文档到GitHub示例,全面探索提升开发技能与解决问题的多元化渠道与实用工具
【8月更文挑战第31天】Xamarin 开发者社区资源概览旨在提升开发效率与解决问题,涵盖官方文档、社区论坛、GitHub 项目等。官方文档详尽,涵盖 Xamarin.Forms 使用、性能优化等;社区论坛供交流心得;GitHub 提供示例代码。此外,第三方博客、视频教程及 Xamarin University 等资源也丰富多样,适合各阶段开发者学习与提升。通过综合利用这些资源,开发者可不断进步,应对技术挑战。
46 0
|
存储 人工智能 Java
ChatGPT API接口编程基础与使用技巧
ChatGPT API接口编程基础与使用技巧
974 0
|
6月前
|
前端开发 JavaScript 小程序
系统刷JavaScripit 构建前端体系(语法篇)
系统刷JavaScripit 构建前端体系(语法篇)
39 1
|
6月前
|
缓存 Java API
Java API设计实战指南:打造稳健、用户友好的API
本文将深入探讨在Java中设计有效API的原则,并着重介绍RESTful设计原则、版本控制策略以及文档实践。
510 38