多语言异常处理用法指南

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

相关文章

使用SQL语言查询多个Excel表格的操作方法

《使用SQL语言查询多个Excel表格的操作方法》本文介绍了如何使用SQL语言查询多个Excel表格,通过将所有Excel表格放入一个.xlsx文件中,并使用pandas和pandasql库进行读取和... 目录如何用SQL语言查询多个Excel表格如何使用sql查询excel内容1. 简介2. 实现思路3

Go语言实现将中文转化为拼音功能

《Go语言实现将中文转化为拼音功能》这篇文章主要为大家详细介绍了Go语言中如何实现将中文转化为拼音功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 有这么一个需求:新用户入职 创建一系列账号比较麻烦,打算通过接口传入姓名进行初始化。想把姓名转化成拼音。因为有些账号即需要中文也需要英

Go语言使用Buffer实现高性能处理字节和字符

《Go语言使用Buffer实现高性能处理字节和字符》在Go中,bytes.Buffer是一个非常高效的类型,用于处理字节数据的读写操作,本文将详细介绍一下如何使用Buffer实现高性能处理字节和... 目录1. bytes.Buffer 的基本用法1.1. 创建和初始化 Buffer1.2. 使用 Writ

深入理解C语言的void*

《深入理解C语言的void*》本文主要介绍了C语言的void*,包括它的任意性、编译器对void*的类型检查以及需要显式类型转换的规则,具有一定的参考价值,感兴趣的可以了解一下... 目录一、void* 的类型任意性二、编译器对 void* 的类型检查三、需要显式类型转换占用的字节四、总结一、void* 的

Python视频处理库VidGear使用小结

《Python视频处理库VidGear使用小结》VidGear是一个高性能的Python视频处理库,本文主要介绍了Python视频处理库VidGear使用小结,文中通过示例代码介绍的非常详细,对大家的... 目录一、VidGear的安装二、VidGear的主要功能三、VidGear的使用示例四、VidGea

Python结合requests和Cheerio处理网页内容的操作步骤

《Python结合requests和Cheerio处理网页内容的操作步骤》Python因其简洁明了的语法和强大的库支持,成为了编写爬虫程序的首选语言之一,requests库是Python中用于发送HT... 目录一、前言二、环境搭建三、requests库的基本使用四、Cheerio库的基本使用五、结合req

使用Python处理CSV和Excel文件的操作方法

《使用Python处理CSV和Excel文件的操作方法》在数据分析、自动化和日常开发中,CSV和Excel文件是非常常见的数据存储格式,ython提供了强大的工具来读取、编辑和保存这两种文件,满足从基... 目录1. CSV 文件概述和处理方法1.1 CSV 文件格式的基本介绍1.2 使用 python 内

如何使用celery进行异步处理和定时任务(django)

《如何使用celery进行异步处理和定时任务(django)》文章介绍了Celery的基本概念、安装方法、如何使用Celery进行异步任务处理以及如何设置定时任务,通过Celery,可以在Web应用中... 目录一、celery的作用二、安装celery三、使用celery 异步执行任务四、使用celery

SpringBoot操作spark处理hdfs文件的操作方法

《SpringBoot操作spark处理hdfs文件的操作方法》本文介绍了如何使用SpringBoot操作Spark处理HDFS文件,包括导入依赖、配置Spark信息、编写Controller和Ser... 目录SpringBoot操作spark处理hdfs文件1、导入依赖2、配置spark信息3、cont

C语言线程池的常见实现方式详解

《C语言线程池的常见实现方式详解》本文介绍了如何使用C语言实现一个基本的线程池,线程池的实现包括工作线程、任务队列、任务调度、线程池的初始化、任务添加、销毁等步骤,感兴趣的朋友跟随小编一起看看吧... 目录1. 线程池的基本结构2. 线程池的实现步骤3. 线程池的核心数据结构4. 线程池的详细实现4.1 初