本文主要是介绍多语言异常处理用法指南,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
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));}
我们可以看到,整个异常处理共分为三部分:
- ServiceException和非ServiceException的日志打印处理
- 语言处理
- 包装错误信息为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方法总结:
作用:
- 将key,相当于code码作为语言查询库Map(ConcurrentHashMap集合)中的key。
- 将locale(语言)和value(msg)放入一个table(ConcurrentHashMap集合)中,作为语言查询库Map中的value
细节:
- 存储多语言数据用ConcurrentHashMap集合是因为,这些资源是标识的static的共享资源,要保证线程安全。
- 如果不需要本地化则不必浪费Map空间,因此语言查询库的初始化大小为0,对象锁也是用的是byte[0]
- put方法如果某一个参数为空,就会抛出异常
- 如果LocaleBundleOptions的escapeSpecialChars属性为true,则将会对value(msg)进行转义字符处理,默认为true
- 最终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()); 进行语种支持处理:
如果数据仓库中包含该语种,则直接返回该语种
如果数据仓库中不包含该语种,但是如果满足以下条件:
- 该语种等于数据仓库中的某个语种的父语种(第一个下划线的左侧内容),返回仓库中那个语种
- 该语种的父语种等于数据仓库的某个语种,返回仓库中的那个语种
- 该语种的父语种等于数据仓库的某个父语种,返回仓库中的那个语种
如果存在对该语种的支持,则直接返回该语种的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,我们来理一下思路:
- 首先判断是否初始化完成
- 尝试从拿着我们传递的语种从数据仓库中找语种message
- 检查我们传递的语种是否存在支持,如果不支持或者我们没有传递语种,则继续,否则返回支持的语种message。
- 尝试从我们的预选语种列表中查找语种,如果能找到语种message,则返回
- 如果还不能找到,则通过我们设置的默认语种从数据仓库中找语种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方法,进行占位符替换处理
走进该方法,我们可以看到:
- 如果占位符包裹符号不满足括号匹配原则(只有左或右)则抛出RuntimeException
- 如果占位符包裹的内容为空,则抛出RuntimeException
- 如果占位符与参数列表索引不匹配,即不在索引范围之内。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,查看语种是否被支持:
- 该语种等于数据仓库中的某个语种的父语种(第一个下划线的左侧内容),返回仓库中那个语种
- 该语种的父语种等于数据仓库的某个语种,返回仓库中的那个语种
- 该语种的父语种等于数据仓库的某个父语种,返回仓库中的那个语种
如果都不满足,则把他当作null进行处理。
key:对应语种文件中.后面的数字
params:参数列表
将参数列表中的索引,作为我们填写在文件中value的占位符,占位符被我们定义的compileStartToken和compileEndToken包裹,对应参数列表中的数据。具体用法参照上一章最后的测试。
- 如果占位符包裹符号不满足括号匹配原则(只有左或右)则抛出RuntimeException
- 如果占位符包裹的内容为空,则抛出RuntimeException
- 如果占位符与参数列表索引不匹配,即不在索引范围之内。CompileOptions的retainKeyIfNull属性为true,则将该占位符内容和包裹字符作为字符串输出,为false(默认),将忽略该占位符和包裹字符,当然前提是我们有传递参数能进入这个方法。
一个小细节:如果传入的数据是基本数据类型的数组,则会编码异常,如果仅传入单个基本数据(非数组)则没有问题。
这篇关于多语言异常处理用法指南的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!