多语言异常处理用法指南

2024-03-09 08:32

本文主要是介绍多语言异常处理用法指南,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

8-9 多语言异常架构初次分析
本文将以程序运行的顺序进行讲解,涉及到的方法都会解释。
末尾会整理所有用法,并列举可能出错的细节问题。

一、引入框架

1.1 基础架构

Exception相关
  • ServiceException
  • …等自定义的ServiceException
DefaultExceptionInterceptor

全局异常处理的地方,我们可以在这里指定哪些异常在这里被拦截处理。
多语言处理工具:

  • recources包下的所有类
  • 基础工具包:utils包下的所有类
  • 错误码:Error
  • 日志:L

1.2 定制多语言

我们可以在类路径下定义多语言包,文件名作为语言名。
这里我引入两种语言:

  • en-US.ini:美式英语
  • zh_CN.ini:中文

需要注意,不同语言文件中

  • 作为同一查询码key 的数量和内容必须一致
  • 作为不同语言的信息value 可以不一样但不能为空

至此,我们已经搭建好了基础框架

二、定制具体ServiceException

对ErrorCode和ServiceException进行扩展,定制特定的异常服务与异常码,可以方便我们对各类异常的管理。
本章节通过对ArticleService服务进行异常服务定制

2.1 ServiceException

    private static final long serialVersionUID = -3121925981104998575L;// 错误码private int errorCode;// 错误参数集合private Object[] errorParams;//错误数据private Map<?, ?> errorData;
  • errorCode:错误码,对应多语言文件的key
  • errorParams:错误参数。异常的扩展信息
  • errorData:错误数据。异常的扩展信息

三、进行统一异常处理

DefaultExceptionInterceptor类是捕获我们指定异常并处理该异常的特定场所,是我们实现多语言处理机制的基石。因此,理解这个类至关重要。
在讲解这个类之前,需要了解以下注解作用:

@ControllerAdvice:@ControllerAdvice是一个@Component,用于定义@ExceptionHandler,@InitBinder和@ModelAttribute方法,适用于所有使用@RequestMapping方法。顾名思义,这是一个增强的 Controller。使用这个 Controller ,可以实现三个方面的功能:

  • 全局异常处理
  • 全局数据绑定
  • 全局数据预处理

在这里我们暂时不讨论全局数据绑定和全局数据预处理。
简单来说,该注解能够通过定义@ExceptionHandler对所有@RequestMapping方法所制造的异常进行捕获,而@RequestMapping方法内部是我们调用Service的地方,因此能够捕获所有的业务异常。

@ExceptionHandler:作用于方法上,属性是异常类对象 ,用于对目标异常进行拦截和处理。

拦截处理逻辑分析

    @ExceptionHandler(Throwable.class) // 在这里我们对所有异常进行捕获public ModelAndView handleError(HttpServletRequest request, HandlerMethod handlerMethod, Throwable ex) {// 1. ServiceException和非ServiceException的处理L.error(ex); // 在日志中打印错误源信息// 如果该异常不是ServiceException,则打印当前request信息if (!(ex instanceof ServiceException)) {ApiLog.log(request, null);}ServiceException se;// 如果不是ServiceException,继续打印错误信息// 将当前异常设置为未知错误的ServiceExceptionif (ex instanceof ServiceException) {se = (ServiceException) ex;} else {L.error(ex);se = new ServiceException(ErrorCode.ERR_UNKNOWN_ERROR);}// 获取错误码int errorCode = se.getErrorCode();// 2. 语言处理// 核心:选择 语言,并传递错误码和错误参数,获取错误MessageString errorMsg = LocaleBundles.getWithArrayParams("sdwe", "err." + errorCode, se.getErrorParams());// 3. 将错误信息包装成Map集合Map<String, Object> error = new HashMap<>();error.put("errcode", errorCode);error.put("errmsg", errorMsg);// 并将我们传递的错误数据添加进入Map集合中if (se.getErrorData() != null) {error.put("errdata", se.getErrorData());}// 将错误信息转化为Json字符串,添加进入ModelAndView的中,放入视图处理的流程中。return new ModelAndView(new JsonView(error));}

我们可以看到,整个异常处理共分为三部分:

  1. ServiceException和非ServiceException的日志打印处理
  2. 语言处理
  3. 包装错误信息为Map集合

四、多语言处理机制

通过对异常处理的分析,我们能看到我们通过
LocaleBundles.getWithArrayParams(“sdwe”, “err.” + errorCode, se.getErrorParams());是多语言机制为我们提供定制化语言信息服务的接口,因此我们需要分析这个方法,并通过这个方法了解整个机制的逻辑。
这一章节建议边调试,边分析。

4.1 LocaleBundleOptions、CompileOptions(配置对象)

LocaleBundleOptions是SimpleLocaleBundle(继承了LocaleBundle)的一个内部类,**包含了SimpleLocaleBundle的相关配置信息。**在初始化SimpleLocaleBundle的时候,会自动将占位配置相关信息添加到内置的CompileOptions(占位配置对象)中。

  • strictMode:是否开启严格模式
  • defaultLocale:默认语言
  • prefLocales:可选语言列表
  • escapeSpecialChars:是否过滤转义符
  • compileStartToken:编译检测占位符的开始字符,默认为${
  • compileEndToken:编译检测占位符的结束字符,默认为}

4.2 SimpleLocaleBundle(LocaleBundle)(多语言实现)

SimpleLocaleBundle是多语言的数据的拥有者和管理者,LocaleBundles通过管理该类,为我们提供使用的接口。

  • writeLock:对象锁,byte[0],节约空间
  • initialized:是否完成初始化标识
  • bundlesMap:语言数据仓库
  • options:配置信息
  • compileOptions:编译信息
    初始化阶段通过构造方法传递配置信息类。

4.3 LocaleBundles(LocaleBundle管理类)

LocaleBundles是我们多语言管理类,内部维护了一个SimpleLocaleBundle公共对象,包含了真正的多语言数据信息。

@StaticInit:该注解是我们自定义的一个注解,在StaticBootstrap中进行处理。利用了反射框架Reflections,在Bean构造之后,对指定包下的所有类进行扫描。过滤获取所有包含@StaticInit注解的类对象,进行一些日志打印处理。不必关注这个东西。

SimpleLocaleBundle的初始化操作

static代码块对BUNDLE(内部维护的SimpleLocaleBundle)进行了一些初始化操作

			// 初始化语种列表String[] locales = new String[]{"zh_CN","en_pea"};// 打印语种信息到日志中L.warn("Locales: " + StringUtil.join(locales, ","));// 配置SimpleLocaleBundleLocaleBundleOptions options = new LocaleBundleOptions();options.setDefaultLocale(locales[1]);options.setPrefLocales(locales); // 配置所有的语言选项列表options.setCompileStartToken("{"); // 设置token开始字符options.setCompileEndToken("}"); // 设置token结束字符BUNDLE = new SimpleLocaleBundle(options); // 将配置信息放入SimpleLocaleBundle中。

接着

		  for (String local : locales) {local = local.trim(); // 去除可选语言空格java.util.Map<String, String> props = FileUtil.readProperties(R.getStream("local/" + local + ".ini"),StringUtil.UTF8, false); // 加载当前语言文件,并将数据放入Map集合中(类路径下的local/xxx.ini文件中读取),在这里并不处理转义字符for (Entry<String, String> entry : props.entrySet()) {String key = StringUtils.trimToNull(entry.getKey()); // 对key去除空格,如果为空返回nullString value = StringUtils.trimToNull(entry.getValue()); // 对value去除空格,如果为空返回nullif (key == null || value == null) {continue;} // 如果有空值,进行下一个entry的迭代BUNDLE.put(key, local, value); // 否则放入本地化数据BUNDLE的语言查询库中,细节见该方法上的注释}}

我们来关注BUNDLE.put方法(向仓库中填充数据

protected void put(String key, String locale, String value) throws Exception {if (StringUtil.isEmpty(key)) {throw new IllegalArgumentException("Bad key");} else if (StringUtil.isEmpty(locale)) {throw new IllegalArgumentException("Bad locale");} else {value = StringUtil.trimToNull(value);if (value == null) {throw new IllegalArgumentException("Bad value");} else {// 配置信息中是否开启字符转义处理if (this.options.escapeSpecialChars) {// 字符转义处理value = StringUtil.escapeSpecialChars(value);}// 加对象锁,防止并发异常synchronized(this.writeLock) {Map<String, String> table = (Map)this.bundlesMap.get(key);if (table == null) {table = new ConcurrentHashMap();this.bundlesMap.put(key, table);}((Map)table).put(locale, value);}}}}

put方法总结:

作用:

  1. 将key,相当于code码作为语言查询库Map(ConcurrentHashMap集合)中的key。
  2. 将locale(语言)和value(msg)放入一个table(ConcurrentHashMap集合)中,作为语言查询库Map中的value

细节:

  1. 存储多语言数据用ConcurrentHashMap集合是因为,这些资源是标识的static的共享资源,要保证线程安全。
  2. 如果不需要本地化则不必浪费Map空间,因此语言查询库的初始化大小为0,对象锁也是用的是byte[0]
  3. put方法如果某一个参数为空,就会抛出异常
  4. 如果LocaleBundleOptions的escapeSpecialChars属性为true,则将会对value(msg)进行转义字符处理,默认为true
  5. 最终Map.put的时候,会加锁,防止并发异常

最后,调用BUNDLE.finishPut();(检验仓库数据的合法性

protected void finishPut() {Set<String> locales = null;Iterator var2 = this.bundlesMap.entrySet().iterator();String key;Set theLocales;do {while(true) {Map table;do {if (!var2.hasNext()) {this.initialized = true;return;}Entry<String, Map<String, String>> bundleEntry = (Entry)var2.next();key = (String)bundleEntry.getKey();table = (Map)bundleEntry.getValue();if (table.get(this.options.getDefaultLocale()) == null) {throw new RuntimeException(this.wrapLogMessage("No default value set for key: " + key, this.options));}} while(!this.options.strictMode);if (locales != null) {theLocales = table.keySet();break;}locales = table.keySet();}} while(theLocales.size() == locales.size() && theLocales.containsAll(locales));throw new RuntimeException(this.wrapLogMessage("Missing some locales for key: " + key, this.options));}

该方法作用总结:

  • 对key去重
  • 如果没有对可选语言列表(options)设置严格模式,验证是否所有语言的key与value都一致,如果存在丢失,则报错
  • 如果都成功的话,则标识初始化成功。
LocaleBundles的getWithArrayParams

该方法是我们获取指定语种Message的接口,了解该接口的用法和细节至关重要。
参数:

  • locale:选择的语种,对应我们的语种文件名
  • key:错误码
  • params:参数列表,对应我们传递过来的参数数组(errorParams)。

进入该方法,我们可以看到 getRaw() 为我们返回了对应的Message,那么进入该方法

首先进行了是否完成舒适化操作的检查(在上一小节的finishPut方法中,如果检查数据合格,则会标识初始化操作成功)

 if (!this.initialized) {throw new RuntimeException("Does not finish init");}

接着去数据仓库中寻找该参数的语种信息,如果该参数没有对应的语种Message信息,则返回null,也就意味着我们在前端不显示数据。

Map<String, String> table = (Map)this.bundlesMap.get(key);if (table == null) {return null;} 

如果存在该key对应的语种信息,则检查是否存在我们需要的语种的message。
这里首先判断我们传入的locale是否为空,如果不为空

else {if (locale != null) {locale = LocaleUtil.findSupportLocale(locale, table.keySet());if (locale != null) {return (String)table.get(locale);}}

LocaleUtil.findSupportLocale(locale, table.keySet()); 进行语种支持处理:
如果数据仓库中包含该语种,则直接返回该语种
如果数据仓库中不包含该语种,但是如果满足以下条件:

  1. 该语种等于数据仓库中的某个语种的父语种(第一个下划线的左侧内容),返回仓库中那个语种
  2. 该语种的父语种等于数据仓库的某个语种,返回仓库中的那个语种
  3. 该语种的父语种等于数据仓库的某个父语种,返回仓库中的那个语种

如果存在对该语种的支持,则直接返回该语种的message信息

继续,如果我们传入的locale为null或不存在对我们传递的语种的支持。
记得我们初始化配置的时候存入的一些预选语种列表吗?this.options.getPrefLocales();就是这个。我们在这里获取到这个列表,对这个列表进行迭代,返回第一个能够在仓库中找到对应语种message的值。

				String[] preferencedLocales = this.options.getPrefLocales();if (preferencedLocales != null) {String[] var5 = preferencedLocales;int var6 = preferencedLocales.length;for(int var7 = 0; var7 < var6; ++var7) {String prefLocale = var5[var7];String value = (String)table.get(prefLocale);if (value != null) {return value;}}}return (String)table.get(this.options.getDefaultLocale());

如果还不能从这里面找到对应的语种信息,则选择我们之前设置的默认语种中的message。

return (String)table.get(this.options.getDefaultLocale());

分析完整个getRow,我们来理一下思路:

  1. 首先判断是否初始化完成
  2. 尝试从拿着我们传递的语种从数据仓库中找语种message
  3. 检查我们传递的语种是否存在支持,如果不支持或者我们没有传递语种,则继续,否则返回支持的语种message。
  4. 尝试从我们的预选语种列表中查找语种,如果能找到语种message,则返回
  5. 如果还不能找到,则通过我们设置的默认语种从数据仓库中找语种message返回

至此,我们完成了对语种message的获取

接着,判断我们是否传入有参数列表,如果没有,直接返回,如果存在,则进行占位符替换处理,具体是怎么处理的呢?

            Map<String, Object> context = new HashMap(params.length);for(int i = 0; i < params.length; ++i) {Object param = params[i];context.put(String.valueOf(i), param);}return PlaceholderUtil.compile(text, context, this.compileOptions, new LocaleBundle.LocaleCompileHandler(locale));}

首先,将参数列表中的值作为value,0-params.lenght作为key放入Map集合中。
再调用PlaceholderUtil.compile方法,进行占位符替换处理
走进该方法,我们可以看到:

  1. 如果占位符包裹符号不满足括号匹配原则(只有左或右)则抛出RuntimeException
  2. 如果占位符包裹的内容为空,则抛出RuntimeException
  3. 如果占位符与参数列表索引不匹配,即不在索引范围之内。CompileOptions的retainKeyIfNull属性为true,则将该占位符内容和包裹字符作为字符串输出,为false(默认),将忽略该占位符和包裹字符,当然前提是我们有传递参数能进入这个方法。

还记得我们初始化配置的时候传入的startToken和endToken参数吗?这个就是设置我们占位符的包裹符号,如果我们没有初始化,我们的占位符默认需要被${}包裹。
该处理是将参数对应{0}、{1}、{2}…与message进行拼接,当然,我们可以对某一个占位符调用多次。
测试:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
至此,我们完成了对整个getWithArrayParams()方法的分析

五、用法总结

5.1 添加多个语种

第一步,添加语种文件,比如添加英文语种en.ini

  • 不同语种文件的key必须相同且数量一致
  • 所有语种文件的value不能为空
  • 语种文件的value可以写占位符,占位符用什么包裹自己配置

第二步,修改仓库的初始化信息
在这里插入图片描述

5.2 配置仓库信息

在这里插入图片描述

  • strictMode:是否开启严格模式
  • defaultLocale:默认的语种
  • prefLocales:预选语种列表
  • escapeSpecialChars:是否不对转义符进行转义处理,默认为true
  • compileStartToken:编译检测占位符的开始字符,默认为${
  • compileEndToken:编译检测占位符的结束字符,默认为}

5.3 getWithArrayParams()参数解释

locale:选择的语种

如果为null,选择预选语种列表的第一个存在于仓库中的语种,如果预选列表没有一个存在于仓库中,则选择默认语种。

如果不为null,查看语种是否被支持:

  1. 该语种等于数据仓库中的某个语种的父语种(第一个下划线的左侧内容),返回仓库中那个语种
  2. 该语种的父语种等于数据仓库的某个语种,返回仓库中的那个语种
  3. 该语种的父语种等于数据仓库的某个父语种,返回仓库中的那个语种
    如果都不满足,则把他当作null进行处理。
key:对应语种文件中.后面的数字
params:参数列表

将参数列表中的索引,作为我们填写在文件中value的占位符,占位符被我们定义的compileStartToken和compileEndToken包裹,对应参数列表中的数据。具体用法参照上一章最后的测试。

  1. 如果占位符包裹符号不满足括号匹配原则(只有左或右)则抛出RuntimeException
  2. 如果占位符包裹的内容为空,则抛出RuntimeException
  3. 如果占位符与参数列表索引不匹配,即不在索引范围之内。CompileOptions的retainKeyIfNull属性为true,则将该占位符内容和包裹字符作为字符串输出,为false(默认),将忽略该占位符和包裹字符,当然前提是我们有传递参数能进入这个方法。

一个小细节:如果传入的数据是基本数据类型的数组,则会编码异常,如果仅传入单个基本数据(非数组)则没有问题。

这篇关于多语言异常处理用法指南的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/790108

相关文章

Java 中的 @SneakyThrows 注解使用方法(简化异常处理的利与弊)

《Java中的@SneakyThrows注解使用方法(简化异常处理的利与弊)》为了简化异常处理,Lombok提供了一个强大的注解@SneakyThrows,本文将详细介绍@SneakyThro... 目录1. @SneakyThrows 简介 1.1 什么是 Lombok?2. @SneakyThrows

在 Spring Boot 中实现异常处理最佳实践

《在SpringBoot中实现异常处理最佳实践》本文介绍如何在SpringBoot中实现异常处理,涵盖核心概念、实现方法、与先前查询的集成、性能分析、常见问题和最佳实践,感兴趣的朋友一起看看吧... 目录一、Spring Boot 异常处理的背景与核心概念1.1 为什么需要异常处理?1.2 Spring B

python处理带有时区的日期和时间数据

《python处理带有时区的日期和时间数据》这篇文章主要为大家详细介绍了如何在Python中使用pytz库处理时区信息,包括获取当前UTC时间,转换为特定时区等,有需要的小伙伴可以参考一下... 目录时区基本信息python datetime使用timezonepandas处理时区数据知识延展时区基本信息

C语言中位操作的实际应用举例

《C语言中位操作的实际应用举例》:本文主要介绍C语言中位操作的实际应用,总结了位操作的使用场景,并指出了需要注意的问题,如可读性、平台依赖性和溢出风险,文中通过代码介绍的非常详细,需要的朋友可以参... 目录1. 嵌入式系统与硬件寄存器操作2. 网络协议解析3. 图像处理与颜色编码4. 高效处理布尔标志集合

Go语言开发实现查询IP信息的MCP服务器

《Go语言开发实现查询IP信息的MCP服务器》随着MCP的快速普及和广泛应用,MCP服务器也层出不穷,本文将详细介绍如何在Go语言中使用go-mcp库来开发一个查询IP信息的MCP... 目录前言mcp-ip-geo 服务器目录结构说明查询 IP 信息功能实现工具实现工具管理查询单个 IP 信息工具的实现服

关于MongoDB图片URL存储异常问题以及解决

《关于MongoDB图片URL存储异常问题以及解决》:本文主要介绍关于MongoDB图片URL存储异常问题以及解决方案,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐... 目录MongoDB图片URL存储异常问题项目场景问题描述原因分析解决方案预防措施js总结MongoDB图

Python Transformers库(NLP处理库)案例代码讲解

《PythonTransformers库(NLP处理库)案例代码讲解》本文介绍transformers库的全面讲解,包含基础知识、高级用法、案例代码及学习路径,内容经过组织,适合不同阶段的学习者,对... 目录一、基础知识1. Transformers 库简介2. 安装与环境配置3. 快速上手示例二、核心模

一文详解Java异常处理你都了解哪些知识

《一文详解Java异常处理你都了解哪些知识》:本文主要介绍Java异常处理的相关资料,包括异常的分类、捕获和处理异常的语法、常见的异常类型以及自定义异常的实现,文中通过代码介绍的非常详细,需要的朋... 目录前言一、什么是异常二、异常的分类2.1 受检异常2.2 非受检异常三、异常处理的语法3.1 try-

Python使用getopt处理命令行参数示例解析(最佳实践)

《Python使用getopt处理命令行参数示例解析(最佳实践)》getopt模块是Python标准库中一个简单但强大的命令行参数处理工具,它特别适合那些需要快速实现基本命令行参数解析的场景,或者需要... 目录为什么需要处理命令行参数?getopt模块基础实际应用示例与其他参数处理方式的比较常见问http

Java Response返回值的最佳处理方案

《JavaResponse返回值的最佳处理方案》在开发Web应用程序时,我们经常需要通过HTTP请求从服务器获取响应数据,这些数据可以是JSON、XML、甚至是文件,本篇文章将详细解析Java中处理... 目录摘要概述核心问题:关键技术点:源码解析示例 1:使用HttpURLConnection获取Resp