自己動(dòng)手在Spring-Boot上加強(qiáng)國(guó)際化功能的示例
前言
公司將項(xiàng)目由Struts2轉(zhuǎn)到Springmvc了,由于公司業(yè)務(wù)是境外服務(wù),所以對(duì)國(guó)際化功能需求很高。Struts2自帶的國(guó)際化功能相對(duì)Springmvc來(lái)說(shuō)更加完善,不過(guò)spring很大的特性就是可定定制化性強(qiáng),所以在公司項(xiàng)目移植的到Springmvc的時(shí)候增加了其國(guó)際化的功能。特此整理記錄并且完善了一下。
本文主要實(shí)現(xiàn)的功能:
從文件夾中直接加載多個(gè)國(guó)際化文件后臺(tái)設(shè)置前端頁(yè)面顯示國(guó)際化信息的文件利用攔截器和注解自動(dòng)設(shè)置前端頁(yè)面顯示國(guó)際化信息的文件
注:本文不詳細(xì)介紹怎么配置國(guó)際化,區(qū)域解析器等。
實(shí)現(xiàn)
國(guó)際化項(xiàng)目初始化
先創(chuàng)建一個(gè)基本的Spring-Boot+thymeleaf+國(guó)際化信息(message.properties)項(xiàng)目,如果有需要可以從我的Github下載。
簡(jiǎn)單看一下項(xiàng)目的目錄和文件
其中I18nApplication.java設(shè)置了一個(gè)CookieLocaleResolver
,采用cookie來(lái)控制國(guó)際化的語(yǔ)言。還設(shè)置一個(gè)LocaleChangeInterceptor
攔截器來(lái)攔截國(guó)際化語(yǔ)言的變化。
@SpringBootApplication @Configuration public class I18nApplication { public static void main(String[] args) { SpringApplication.run(I18nApplication.class, args); } @Bean public LocaleResolver localeResolver() { CookieLocaleResolver slr = new CookieLocaleResolver(); slr.setCookieMaxAge(3600); slr.setCookieName("Language");//設(shè)置存儲(chǔ)的Cookie的name為L(zhǎng)anguage return slr; } @Bean public WebMvcConfigurer webMvcConfigurer() { return new WebMvcConfigurer() { //攔截器 @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LocaleChangeInterceptor()).addPathPatterns("/**"); } }; } }
我們?cè)倏匆幌耯ello.html中寫(xiě)了什么:
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"> <head> <title>Hello World!</title> </head> <body> <h1 th:text="#{i18n_page}"></h1> <h3 th:text="#{hello}"></h3> </body> </html>
現(xiàn)在啟動(dòng)項(xiàng)目并且訪問(wèn)http://localhost:9090/hello
(我在application.properties)中設(shè)置了端口為9090。
由于瀏覽器默認(rèn)的語(yǔ)言是中文,所以他默認(rèn)會(huì)去messages_zh_CN.properties中找,如果沒(méi)有就會(huì)去messages.properties中找國(guó)際化詞。
然后我們?cè)跒g覽器中輸入http://localhost:9090/hello?locale=en_US
,語(yǔ)言就會(huì)切到英文。同樣的如果url后參數(shù)設(shè)置為locale=zh_CH
,語(yǔ)言就會(huì)切到中文。
從文件夾中直接加載多個(gè)國(guó)際化文件
在我們hello.html頁(yè)面中,只有'i18n_page'和'hello'兩個(gè)國(guó)際化信息,然而在實(shí)際項(xiàng)目中肯定不會(huì)只有幾個(gè)國(guó)際化信息那么少,通常都是成千上百個(gè)的,那我們肯定不能把這么多的國(guó)際化信息都放在messages.properties
一個(gè)文件中,通常都是把國(guó)際化信息分類存放在幾個(gè)文件中。但是當(dāng)項(xiàng)目大了以后,這些國(guó)際化文件也會(huì)越來(lái)越多,這時(shí)候在application.properties
文件中一個(gè)個(gè)的去配置這個(gè)文件也是不方便的,所以現(xiàn)在我們實(shí)現(xiàn)一個(gè)功能自動(dòng)加載制定目錄下所有的國(guó)際化文件。
繼承ResourceBundleMessageSource
在項(xiàng)目下創(chuàng)建一個(gè)類繼承ResourceBundleMessageSource
或者ReloadableResourceBundleMessageSource
,起名為MessageResourceExtension
。并且注入到bean中起名為messageSource
,這里我們繼承ResourceBundleMessageSource。
@Component("messageSource") public class MessageResourceExtension extends ResourceBundleMessageSource { }
注意這里我們的Component名字必須為'messageSource',因?yàn)樵诔跏蓟?code>ApplicationContext的時(shí)候,會(huì)查找bean名為'messageSource'的bean。這個(gè)過(guò)程在AbstractApplicationContext.java
中,我們看一下源代碼
/** * Initialize the MessageSource. * Use parent's if none defined in this context. */ protected void initMessageSource() { ConfigurableListableBeanFactory beanFactory = getBeanFactory(); if (beanFactory.containsLocalBean(MESSAGE_SOURCE_BEAN_NAME)) { this.messageSource = beanFactory.getBean(MESSAGE_SOURCE_BEAN_NAME, MessageSource.class); ... } } ...
在這個(gè)初始化MessageSource的方法中,beanFactory查找注入名為MESSAGE_SOURCE_BEAN_NAME(messageSource)
的bean,如果沒(méi)有找到,就會(huì)在其父類中查找是否有該名的bean。
實(shí)現(xiàn)文件加載
現(xiàn)在我們可以開(kāi)始在剛才創(chuàng)建的MessageResourceExtension
中寫(xiě)加載文件的方法了。
@Component("messageSource") public class MessageResourceExtension extends ResourceBundleMessageSource { private final static Logger logger = LoggerFactory.getLogger(MessageResourceExtension.class); /** * 指定的國(guó)際化文件目錄 */ @Value(value = "${spring.messages.baseFolder:i18n}") private String baseFolder; /** * 父MessageSource指定的國(guó)際化文件 */ @Value(value = "${spring.messages.basename:message}") private String basename; @PostConstruct public void init() { logger.info("init MessageResourceExtension..."); if (!StringUtils.isEmpty(baseFolder)) { try { this.setBasenames(getAllBaseNames(baseFolder)); } catch (IOException e) { logger.error(e.getMessage()); } } //設(shè)置父MessageSource ResourceBundleMessageSource parent = new ResourceBundleMessageSource(); parent.setBasename(basename); this.setParentMessageSource(parent); } /** * 獲取文件夾下所有的國(guó)際化文件名 * * @param folderName 文件名 * @return * @throws IOException */ private String[] getAllBaseNames(String folderName) throws IOException { Resource resource = new ClassPathResource(folderName); File file = resource.getFile(); List<String> baseNames = new ArrayList<>(); if (file.exists() && file.isDirectory()) { this.getAllFile(baseNames, file, ""); } else { logger.error("指定的baseFile不存在或者不是文件夾"); } return baseNames.toArray(new String[baseNames.size()]); } /** * 遍歷所有文件 * * @param basenames * @param folder * @param path */ private void getAllFile(List<String> basenames, File folder, String path) { if (folder.isDirectory()) { for (File file : folder.listFiles()) { this.getAllFile(basenames, file, path + folder.getName() + File.separator); } } else { String i18Name = this.getI18FileName(path + folder.getName()); if (!basenames.contains(i18Name)) { basenames.add(i18Name); } } } /** * 把普通文件名轉(zhuǎn)換成國(guó)際化文件名 * * @param filename * @return */ private String getI18FileName(String filename) { filename = filename.replace(".properties", ""); for (int i = 0; i < 2; i++) { int index = filename.lastIndexOf("_"); if (index != -1) { filename = filename.substring(0, index); } } return filename; } }
依次解釋一下幾個(gè)方法。
init()
方法上有一個(gè)@PostConstruct
注解,這會(huì)在MessageResourceExtension類被實(shí)例化之后自動(dòng)調(diào)用init()
方法。這個(gè)方法獲取到baseFolder
目錄下所有的國(guó)際化文件并設(shè)置到basenameSet
中。并且設(shè)置一個(gè)ParentMessageSource
,這會(huì)在找不到國(guó)際化信息的時(shí)候,調(diào)用父MessageSource來(lái)查找國(guó)際化信息。getAllBaseNames()
方法獲取到baseFolder
的路徑,然后調(diào)用getAllFile()
方法獲取到該目錄下所有的國(guó)際化文件的文件名。getAllFile()
遍歷目錄,如果是文件夾就繼續(xù)遍歷,如果是文件就調(diào)用getI18FileName()
把文件名轉(zhuǎn)為'i18n/basename/‘格式的國(guó)際化資源名。
所以簡(jiǎn)單來(lái)說(shuō)就是在MessageResourceExtension
被實(shí)例化之后,把'i18n'文件夾下的資源文件的名字,加載到Basenames
中?,F(xiàn)在來(lái)看一下效果。
首先我們?cè)赼pplication.properties文件中添加一個(gè)spring.messages.baseFolder=i18n
,這會(huì)把'i18n'這個(gè)值賦值給MessageResourceExtension
中的baseFolder
。
在啟動(dòng)后看到控制臺(tái)里打印出了init信息,表示被@PostConstruct
注解的init()方法已經(jīng)執(zhí)行。
然后我們?cè)賱?chuàng)建兩組國(guó)際化信息文件:'dashboard'和'merchant',里面分別只有一個(gè)國(guó)際化信息:'dashboard.hello'和'merchant.hello'。
之后再修改一下hello.html文件,然后訪問(wèn)hello頁(yè)面。
... <body> <h1>國(guó)際化頁(yè)面!</h1> <p th:text="#{hello}"></p> <p th:text="#{merchant.hello}"></p> <p th:text="#{dashboard.hello}"></p> </body> ...
可以看到網(wǎng)頁(yè)中加載了'message','dashboard'和'merchant'中的國(guó)際化信息,說(shuō)明我們已經(jīng)成功一次性加載了'i18n'文件夾下的文件。
后臺(tái)設(shè)置前端頁(yè)面顯示國(guó)際化信息的文件
s剛才那一節(jié)我們成功加載了多個(gè)國(guó)際化文件并顯示出了他們的國(guó)際化信息。但是'dashboard.properties'中的國(guó)際化信息為'dashboard.hello'而'merchant.properties'中的是'merchant.hello',這樣每個(gè)都要寫(xiě)一個(gè)前綴豈不是很麻煩,現(xiàn)在我想要在'dashboard'和'merchant'的國(guó)際化文件中都只寫(xiě)'hello'但是顯示的是'dashboard'或'merchant'中的國(guó)際化信息。
在MessageResourceExtension
重寫(xiě)resolveCodeWithoutArguments
方法(如果有字符格式化的需求就重寫(xiě)resolveCode
方法)。
@Component("messageSource") public class MessageResourceExtension extends ResourceBundleMessageSource { ... public static String I18N_ATTRIBUTE = "i18n_attribute"; @Override protected String resolveCodeWithoutArguments(String code, Locale locale) { // 獲取request中設(shè)置的指定國(guó)際化文件名 ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes(); final String i18File = (String) attr.getAttribute(I18N_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST); if (!StringUtils.isEmpty(i18File)) { //獲取在basenameSet中匹配的國(guó)際化文件名 String basename = getBasenameSet().stream() .filter(name -> StringUtils.endsWithIgnoreCase(name, i18File)) .findFirst().orElse(null); if (!StringUtils.isEmpty(basename)) { //得到指定的國(guó)際化文件資源 ResourceBundle bundle = getResourceBundle(basename, locale); if (bundle != null) { return getStringOrNull(bundle, code); } } } //如果指定i18文件夾中沒(méi)有該國(guó)際化字段,返回null會(huì)在ParentMessageSource中查找 return null; } ... }
在我們重寫(xiě)的resolveCodeWithoutArguments
方法中,從HttpServletRequest中獲取到‘I18N_ATTRIBUTE'(等下再說(shuō)這個(gè)在哪里設(shè)置),這個(gè)對(duì)應(yīng)我們想要顯示的國(guó)際化文件名,然后我們?cè)?code>BasenameSet中查找該文件,再通過(guò)getResourceBundle
獲取到資源,最后再getStringOrNull
獲取到對(duì)應(yīng)的國(guó)際化信息。
現(xiàn)在我們到我們的HelloController
里加兩個(gè)方法。
@Controller public class HelloController { @GetMapping("/hello") public String index(HttpServletRequest request) { request.setAttribute(MessageResourceExtension.I18N_ATTRIBUTE, "hello"); return "system/hello"; } @GetMapping("/dashboard") public String dashboard(HttpServletRequest request) { request.setAttribute(MessageResourceExtension.I18N_ATTRIBUTE, "dashboard"); return "dashboard"; } @GetMapping("/merchant") public String merchant(HttpServletRequest request) { request.setAttribute(MessageResourceExtension.I18N_ATTRIBUTE, "merchant"); return "merchant"; } }
看到我們?cè)诿總€(gè)方法中都設(shè)置一個(gè)對(duì)應(yīng)的'I18N_ATTRIBUTE',這會(huì)在每次請(qǐng)求中設(shè)置對(duì)應(yīng)的國(guó)際化文件,然后在MessageResourceExtension
中獲取。
這時(shí)我們看一下我們的國(guó)際化文件,我們可以看到所有關(guān)鍵字都是'hello',但是信息卻不同。
同時(shí)新增兩個(gè)html文件分別是'dashboard.html'和'merchant.html',里面只有一個(gè)'hello'的國(guó)際化信息和用于區(qū)分的標(biāo)題。
<!-- 這是hello.html --> <body> <h1>國(guó)際化頁(yè)面!</h1> <p th:text="#{hello}"></p> </body>
<!-- 這是dashboard.html --> <body> <h1>國(guó)際化頁(yè)面(dashboard)!</h1> <p th:text="#{hello}"></p> </body>
<!-- 這是merchant.html --> <body> <h1>國(guó)際化頁(yè)面(merchant)!</h1> <p th:text="#{hello}"></p> </body>
這時(shí)我們啟動(dòng)項(xiàng)目看一下。
可以看到雖然在每個(gè)頁(yè)面的國(guó)際化詞都是'hello',但是我們?cè)趯?duì)應(yīng)的頁(yè)面顯示了我們想要顯示的信息。
利用攔截器和注解自動(dòng)設(shè)置前端頁(yè)面顯示國(guó)際化信息的文件
雖然已經(jīng)可以指定對(duì)應(yīng)的國(guó)際化信息,但是這樣要在每個(gè)controller里的HttpServletRequest中設(shè)置國(guó)際化文件實(shí)在太麻煩了,所以現(xiàn)在我們實(shí)現(xiàn)自動(dòng)判定來(lái)顯示對(duì)應(yīng)的文件。
首先我們創(chuàng)建一個(gè)注解,這個(gè)注解可以放在類上或者方法上。
@Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface I18n { /** * 國(guó)際化文件名 */ String value(); }
然后我們把這個(gè)創(chuàng)建的I18n
注解放在剛才的Controller方法中,為了顯示他的效果,我們?cè)賱?chuàng)建一個(gè)ShopController
和UserController
,同時(shí)也創(chuàng)建對(duì)應(yīng)的'shop'和'user'的國(guó)際化文件,內(nèi)容也都是一個(gè)'hello'。
@Controller public class HelloController { @GetMapping("/hello") public String index() { return "system/hello"; } @I18n("dashboard") @GetMapping("/dashboard") public String dashboard() { return "dashboard"; } @I18n("merchant") @GetMapping("/merchant") public String merchant() { return "merchant"; } }
@I18n("shop") @Controller public class ShopController { @GetMapping("shop") public String shop() { return "shop"; } }
@Controller public class UserController { @GetMapping("user") public String user() { return "user"; } }
我們把I18n
注解分別放在HelloController
下的dashboard
和merchant
方法下,和ShopController
類上。并且去除了原來(lái)dashboard
和merchant
方法下設(shè)置‘I18N_ATTRIBUTE'的語(yǔ)句。
準(zhǔn)備工作都做好了,現(xiàn)在看看如何實(shí)現(xiàn)根據(jù)這些注解自動(dòng)的指定國(guó)際化文件。
public class MessageResourceInterceptor implements HandlerInterceptor { @Override public void postHandle(HttpServletRequest req, HttpServletResponse rep, Object handler, ModelAndView modelAndView) { // 在方法中設(shè)置i18路徑 if (null != req.getAttribute(MessageResourceExtension.I18N_ATTRIBUTE)) { return; } HandlerMethod method = (HandlerMethod) handler; // 在method上注解了i18 I18n i18nMethod = method.getMethodAnnotation(I18n.class); if (null != i18nMethod) { req.setAttribute(MessageResourceExtension.I18N_ATTRIBUTE, i18nMethod.value()); return; } // 在Controller上注解了i18 I18n i18nController = method.getBeanType().getAnnotation(I18n.class); if (null != i18nController) { req.setAttribute(MessageResourceExtension.I18N_ATTRIBUTE, i18nController.value()); return; } // 根據(jù)Controller名字設(shè)置i18 String controller = method.getBeanType().getName(); int index = controller.lastIndexOf("."); if (index != -1) { controller = controller.substring(index + 1, controller.length()); } index = controller.toUpperCase().indexOf("CONTROLLER"); if (index != -1) { controller = controller.substring(0, index); } req.setAttribute(MessageResourceExtension.I18N_ATTRIBUTE, controller); } @Override public boolean preHandle(HttpServletRequest req, HttpServletResponse rep, Object handler) { // 在跳轉(zhuǎn)到該方法先清除request中的國(guó)際化信息 req.removeAttribute(MessageResourceExtension.I18N_ATTRIBUTE); return true; } }
簡(jiǎn)單講解一下這個(gè)攔截器。
首先,如果request中已經(jīng)有'I18N_ATTRIBUTE',說(shuō)明在Controller的方法中指定設(shè)置了,就不再判斷。
然后判斷一下進(jìn)入攔截器的方法上有沒(méi)有I18n
的注解,如果有就設(shè)置'I18N_ATTRIBUTE'到request中并退出攔截器,如果沒(méi)有就繼續(xù)。
再判斷進(jìn)入攔截的類上有沒(méi)有I18n
的注解,如果有就設(shè)置'I18N_ATTRIBUTE'到request中并退出攔截器,如果沒(méi)有就繼續(xù)。
最后假如方法和類上都沒(méi)有I18n
的注解,那我們可以根據(jù)Controller名自動(dòng)設(shè)置指定的國(guó)際化文件,比如'UserController'那么就會(huì)去找'user'的國(guó)際化文件。
現(xiàn)在我們?cè)龠\(yùn)行一下看看效果,看到每個(gè)鏈接都顯示的他們對(duì)應(yīng)的國(guó)際化信息里的內(nèi)容。
最后
剛才完成了我們整個(gè)國(guó)際化增強(qiáng)的基本功能,最后我把全部代碼整理了一下,并且整合了bootstrap4來(lái)展示了一下功能的實(shí)現(xiàn)效果。
詳細(xì)的代碼可以看我Github上Spring-Boot-I18n-Pro的代碼
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- Spring Boot實(shí)現(xiàn)通用的接口參數(shù)校驗(yàn)
- SpringBoot如何優(yōu)雅的處理校驗(yàn)參數(shù)的方法
- 詳解如何在Spring Boot項(xiàng)目使用參數(shù)校驗(yàn)
- Spring boot進(jìn)行參數(shù)校驗(yàn)的方法實(shí)例詳解
- Spring Boot 參數(shù)校驗(yàn)的具體實(shí)現(xiàn)方式
- 詳解SpringBoot中的參數(shù)校驗(yàn)(項(xiàng)目實(shí)戰(zhàn))
- springboot+thymeleaf國(guó)際化之LocaleResolver接口的示例
- SpringBoot實(shí)現(xiàn)國(guó)際化過(guò)程詳解
- Spring Boot Thymeleaf實(shí)現(xiàn)國(guó)際化的方法詳解
- Spring Boot REST國(guó)際化的實(shí)現(xiàn)代碼
- SpringBoot參數(shù)校驗(yàn)與國(guó)際化使用教程
相關(guān)文章
Java Fluent Mybatis實(shí)戰(zhàn)之構(gòu)建項(xiàng)目與代碼生成篇上
Java中常用的ORM框架主要是mybatis, hibernate, JPA等框架。國(guó)內(nèi)又以Mybatis用的多,基于mybatis上的增強(qiáng)框架,又有mybatis plus和TK mybatis等。今天我們介紹一個(gè)新的mybatis增強(qiáng)框架 fluent mybatis2021-10-10細(xì)數(shù)java中Long與Integer比較容易犯的錯(cuò)誤總結(jié)
下面小編就為大家?guī)?lái)一篇細(xì)數(shù)java中Long與Integer比較容易犯的錯(cuò)誤總結(jié)。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-01-01IntelliJ IDEA Project窗口的一些設(shè)置詳解
這篇文章主要介紹了IntelliJ IDEA Project窗口的一些設(shè)置詳解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-08-08JAVA中時(shí)間戳與LocalDateTime互相轉(zhuǎn)換代碼例子
最近在編碼過(guò)程中遇到將時(shí)間戳轉(zhuǎn)化為 LocalDateTime,所以這里給總結(jié)下,這篇文章主要給大家介紹了關(guān)于JAVA中時(shí)間戳與LocalDateTime互相轉(zhuǎn)換的相關(guān)資料,需要的朋友可以參考下2023-11-11