关于后端:无缝对接多语言参数校验的终极指南一

28次阅读

共计 7775 个字符,预计需要花费 20 分钟才能阅读完成。

前言

  在此之前,写过在两篇文章,是对于如何在 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 内实现热更新呢?咱们下期持续聊。

结尾

  如果感觉对你有帮忙,能够多多评论,多多点赞哦,也能够到我的主页看看,说不定有你喜爱的文章,也能够顺手点个关注哦,谢谢。

  我是不一样的科技宅,每天提高一点点,体验不一样的生存。咱们下期见!

正文完
 0