多语言异常处理用法指南

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

相关文章

python使用fastapi实现多语言国际化的操作指南

《python使用fastapi实现多语言国际化的操作指南》本文介绍了使用Python和FastAPI实现多语言国际化的操作指南,包括多语言架构技术栈、翻译管理、前端本地化、语言切换机制以及常见陷阱和... 目录多语言国际化实现指南项目多语言架构技术栈目录结构翻译工作流1. 翻译数据存储2. 翻译生成脚本

Go语言中三种容器类型的数据结构详解

《Go语言中三种容器类型的数据结构详解》在Go语言中,有三种主要的容器类型用于存储和操作集合数据:本文主要介绍三者的使用与区别,感兴趣的小伙伴可以跟随小编一起学习一下... 目录基本概念1. 数组(Array)2. 切片(Slice)3. 映射(Map)对比总结注意事项基本概念在 Go 语言中,有三种主要

使用C++将处理后的信号保存为PNG和TIFF格式

《使用C++将处理后的信号保存为PNG和TIFF格式》在信号处理领域,我们常常需要将处理结果以图像的形式保存下来,方便后续分析和展示,C++提供了多种库来处理图像数据,本文将介绍如何使用stb_ima... 目录1. PNG格式保存使用stb_imagephp_write库1.1 安装和包含库1.2 代码解

C语言中自动与强制转换全解析

《C语言中自动与强制转换全解析》在编写C程序时,类型转换是确保数据正确性和一致性的关键环节,无论是隐式转换还是显式转换,都各有特点和应用场景,本文将详细探讨C语言中的类型转换机制,帮助您更好地理解并在... 目录类型转换的重要性自动类型转换(隐式转换)强制类型转换(显式转换)常见错误与注意事项总结与建议类型

C#使用DeepSeek API实现自然语言处理,文本分类和情感分析

《C#使用DeepSeekAPI实现自然语言处理,文本分类和情感分析》在C#中使用DeepSeekAPI可以实现多种功能,例如自然语言处理、文本分类、情感分析等,本文主要为大家介绍了具体实现步骤,... 目录准备工作文本生成文本分类问答系统代码生成翻译功能文本摘要文本校对图像描述生成总结在C#中使用Deep

Go语言利用泛型封装常见的Map操作

《Go语言利用泛型封装常见的Map操作》Go语言在1.18版本中引入了泛型,这是Go语言发展的一个重要里程碑,它极大地增强了语言的表达能力和灵活性,本文将通过泛型实现封装常见的Map操作,感... 目录什么是泛型泛型解决了什么问题Go泛型基于泛型的常见Map操作代码合集总结什么是泛型泛型是一种编程范式,允

Spring Boot 整合 ShedLock 处理定时任务重复执行的问题小结

《SpringBoot整合ShedLock处理定时任务重复执行的问题小结》ShedLock是解决分布式系统中定时任务重复执行问题的Java库,通过在数据库中加锁,确保只有一个节点在指定时间执行... 目录前言什么是 ShedLock?ShedLock 的工作原理:定时任务重复执行China编程的问题使用 Shed

Redis如何使用zset处理排行榜和计数问题

《Redis如何使用zset处理排行榜和计数问题》Redis的ZSET数据结构非常适合处理排行榜和计数问题,它可以在高并发的点赞业务中高效地管理点赞的排名,并且由于ZSET的排序特性,可以轻松实现根据... 目录Redis使用zset处理排行榜和计数业务逻辑ZSET 数据结构优化高并发的点赞操作ZSET 结

微服务架构之使用RabbitMQ进行异步处理方式

《微服务架构之使用RabbitMQ进行异步处理方式》本文介绍了RabbitMQ的基本概念、异步调用处理逻辑、RabbitMQ的基本使用方法以及在SpringBoot项目中使用RabbitMQ解决高并发... 目录一.什么是RabbitMQ?二.异步调用处理逻辑:三.RabbitMQ的基本使用1.安装2.架构

Android kotlin语言实现删除文件的解决方案

《Androidkotlin语言实现删除文件的解决方案》:本文主要介绍Androidkotlin语言实现删除文件的解决方案,在项目开发过程中,尤其是需要跨平台协作的项目,那么删除用户指定的文件的... 目录一、前言二、适用环境三、模板内容1.权限申请2.Activity中的模板一、前言在项目开发过程中,尤