自己動手在Spring-Boot上加強國際化功能的示例
前言
公司將項目由Struts2轉到Springmvc了,由于公司業(yè)務是境外服務,所以對國際化功能需求很高。Struts2自帶的國際化功能相對Springmvc來說更加完善,不過spring很大的特性就是可定定制化性強,所以在公司項目移植的到Springmvc的時候增加了其國際化的功能。特此整理記錄并且完善了一下。
本文主要實現(xiàn)的功能:
從文件夾中直接加載多個國際化文件后臺設置前端頁面顯示國際化信息的文件利用攔截器和注解自動設置前端頁面顯示國際化信息的文件
注:本文不詳細介紹怎么配置國際化,區(qū)域解析器等。
實現(xiàn)
國際化項目初始化
先創(chuàng)建一個基本的Spring-Boot+thymeleaf+國際化信息(message.properties)項目,如果有需要可以從我的Github下載。
簡單看一下項目的目錄和文件

其中I18nApplication.java設置了一個CookieLocaleResolver,采用cookie來控制國際化的語言。還設置一個LocaleChangeInterceptor攔截器來攔截國際化語言的變化。
@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");//設置存儲的Cookie的name為Language
return slr;
}
@Bean
public WebMvcConfigurer webMvcConfigurer() {
return new WebMvcConfigurer() {
//攔截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LocaleChangeInterceptor()).addPathPatterns("/**");
}
};
}
}
我們再看一下hello.html中寫了什么:
<!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)在啟動項目并且訪問http://localhost:9090/hello(我在application.properties)中設置了端口為9090。

由于瀏覽器默認的語言是中文,所以他默認會去messages_zh_CN.properties中找,如果沒有就會去messages.properties中找國際化詞。
然后我們在瀏覽器中輸入http://localhost:9090/hello?locale=en_US,語言就會切到英文。同樣的如果url后參數(shù)設置為locale=zh_CH,語言就會切到中文。

從文件夾中直接加載多個國際化文件
在我們hello.html頁面中,只有'i18n_page'和'hello'兩個國際化信息,然而在實際項目中肯定不會只有幾個國際化信息那么少,通常都是成千上百個的,那我們肯定不能把這么多的國際化信息都放在messages.properties一個文件中,通常都是把國際化信息分類存放在幾個文件中。但是當項目大了以后,這些國際化文件也會越來越多,這時候在application.properties文件中一個個的去配置這個文件也是不方便的,所以現(xiàn)在我們實現(xiàn)一個功能自動加載制定目錄下所有的國際化文件。
繼承ResourceBundleMessageSource
在項目下創(chuàng)建一個類繼承ResourceBundleMessageSource或者ReloadableResourceBundleMessageSource,起名為MessageResourceExtension。并且注入到bean中起名為messageSource,這里我們繼承ResourceBundleMessageSource。
@Component("messageSource")
public class MessageResourceExtension extends ResourceBundleMessageSource {
}
注意這里我們的Component名字必須為'messageSource',因為在初始化ApplicationContext的時候,會查找bean名為'messageSource'的bean。這個過程在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);
...
}
}
...
在這個初始化MessageSource的方法中,beanFactory查找注入名為MESSAGE_SOURCE_BEAN_NAME(messageSource)的bean,如果沒有找到,就會在其父類中查找是否有該名的bean。
實現(xiàn)文件加載
現(xiàn)在我們可以開始在剛才創(chuàng)建的MessageResourceExtension
中寫加載文件的方法了。
@Component("messageSource")
public class MessageResourceExtension extends ResourceBundleMessageSource {
private final static Logger logger = LoggerFactory.getLogger(MessageResourceExtension.class);
/**
* 指定的國際化文件目錄
*/
@Value(value = "${spring.messages.baseFolder:i18n}")
private String baseFolder;
/**
* 父MessageSource指定的國際化文件
*/
@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());
}
}
//設置父MessageSource
ResourceBundleMessageSource parent = new ResourceBundleMessageSource();
parent.setBasename(basename);
this.setParentMessageSource(parent);
}
/**
* 獲取文件夾下所有的國際化文件名
*
* @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);
}
}
}
/**
* 把普通文件名轉換成國際化文件名
*
* @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;
}
}
依次解釋一下幾個方法。
init()方法上有一個@PostConstruct注解,這會在MessageResourceExtension類被實例化之后自動調用init()方法。這個方法獲取到baseFolder目錄下所有的國際化文件并設置到basenameSet中。并且設置一個ParentMessageSource,這會在找不到國際化信息的時候,調用父MessageSource來查找國際化信息。getAllBaseNames()方法獲取到baseFolder的路徑,然后調用getAllFile()方法獲取到該目錄下所有的國際化文件的文件名。getAllFile()遍歷目錄,如果是文件夾就繼續(xù)遍歷,如果是文件就調用getI18FileName()把文件名轉為'i18n/basename/‘格式的國際化資源名。
所以簡單來說就是在MessageResourceExtension被實例化之后,把'i18n'文件夾下的資源文件的名字,加載到Basenames中?,F(xiàn)在來看一下效果。
首先我們在application.properties文件中添加一個spring.messages.baseFolder=i18n,這會把'i18n'這個值賦值給MessageResourceExtension中的baseFolder。
在啟動后看到控制臺里打印出了init信息,表示被@PostConstruct注解的init()方法已經執(zhí)行。

然后我們再創(chuàng)建兩組國際化信息文件:'dashboard'和'merchant',里面分別只有一個國際化信息:'dashboard.hello'和'merchant.hello'。

之后再修改一下hello.html文件,然后訪問hello頁面。
...
<body>
<h1>國際化頁面!</h1>
<p th:text="#{hello}"></p>
<p th:text="#{merchant.hello}"></p>
<p th:text="#{dashboard.hello}"></p>
</body>
...


可以看到網頁中加載了'message','dashboard'和'merchant'中的國際化信息,說明我們已經成功一次性加載了'i18n'文件夾下的文件。
后臺設置前端頁面顯示國際化信息的文件
s剛才那一節(jié)我們成功加載了多個國際化文件并顯示出了他們的國際化信息。但是'dashboard.properties'中的國際化信息為'dashboard.hello'而'merchant.properties'中的是'merchant.hello',這樣每個都要寫一個前綴豈不是很麻煩,現(xiàn)在我想要在'dashboard'和'merchant'的國際化文件中都只寫'hello'但是顯示的是'dashboard'或'merchant'中的國際化信息。
在MessageResourceExtension重寫resolveCodeWithoutArguments方法(如果有字符格式化的需求就重寫resolveCode方法)。
@Component("messageSource")
public class MessageResourceExtension extends ResourceBundleMessageSource {
...
public static String I18N_ATTRIBUTE = "i18n_attribute";
@Override
protected String resolveCodeWithoutArguments(String code, Locale locale) {
// 獲取request中設置的指定國際化文件名
ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
final String i18File = (String) attr.getAttribute(I18N_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST);
if (!StringUtils.isEmpty(i18File)) {
//獲取在basenameSet中匹配的國際化文件名
String basename = getBasenameSet().stream()
.filter(name -> StringUtils.endsWithIgnoreCase(name, i18File))
.findFirst().orElse(null);
if (!StringUtils.isEmpty(basename)) {
//得到指定的國際化文件資源
ResourceBundle bundle = getResourceBundle(basename, locale);
if (bundle != null) {
return getStringOrNull(bundle, code);
}
}
}
//如果指定i18文件夾中沒有該國際化字段,返回null會在ParentMessageSource中查找
return null;
}
...
}
在我們重寫的resolveCodeWithoutArguments方法中,從HttpServletRequest中獲取到‘I18N_ATTRIBUTE'(等下再說這個在哪里設置),這個對應我們想要顯示的國際化文件名,然后我們在BasenameSet中查找該文件,再通過getResourceBundle獲取到資源,最后再getStringOrNull獲取到對應的國際化信息。
現(xiàn)在我們到我們的HelloController里加兩個方法。
@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";
}
}
看到我們在每個方法中都設置一個對應的'I18N_ATTRIBUTE',這會在每次請求中設置對應的國際化文件,然后在MessageResourceExtension中獲取。
這時我們看一下我們的國際化文件,我們可以看到所有關鍵字都是'hello',但是信息卻不同。



同時新增兩個html文件分別是'dashboard.html'和'merchant.html',里面只有一個'hello'的國際化信息和用于區(qū)分的標題。
<!-- 這是hello.html -->
<body>
<h1>國際化頁面!</h1>
<p th:text="#{hello}"></p>
</body>
<!-- 這是dashboard.html -->
<body>
<h1>國際化頁面(dashboard)!</h1>
<p th:text="#{hello}"></p>
</body>
<!-- 這是merchant.html -->
<body>
<h1>國際化頁面(merchant)!</h1>
<p th:text="#{hello}"></p>
</body>
這時我們啟動項目看一下。



可以看到雖然在每個頁面的國際化詞都是'hello',但是我們在對應的頁面顯示了我們想要顯示的信息。
利用攔截器和注解自動設置前端頁面顯示國際化信息的文件
雖然已經可以指定對應的國際化信息,但是這樣要在每個controller里的HttpServletRequest中設置國際化文件實在太麻煩了,所以現(xiàn)在我們實現(xiàn)自動判定來顯示對應的文件。
首先我們創(chuàng)建一個注解,這個注解可以放在類上或者方法上。
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface I18n {
/**
* 國際化文件名
*/
String value();
}
然后我們把這個創(chuàng)建的I18n 注解放在剛才的Controller方法中,為了顯示他的效果,我們再創(chuàng)建一個ShopController和UserController,同時也創(chuàng)建對應的'shop'和'user'的國際化文件,內容也都是一個'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類上。并且去除了原來dashboard和merchant方法下設置‘I18N_ATTRIBUTE'的語句。
準備工作都做好了,現(xiàn)在看看如何實現(xiàn)根據(jù)這些注解自動的指定國際化文件。
public class MessageResourceInterceptor implements HandlerInterceptor {
@Override
public void postHandle(HttpServletRequest req, HttpServletResponse rep, Object handler, ModelAndView modelAndView) {
// 在方法中設置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名字設置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) {
// 在跳轉到該方法先清除request中的國際化信息
req.removeAttribute(MessageResourceExtension.I18N_ATTRIBUTE);
return true;
}
}
簡單講解一下這個攔截器。
首先,如果request中已經有'I18N_ATTRIBUTE',說明在Controller的方法中指定設置了,就不再判斷。
然后判斷一下進入攔截器的方法上有沒有I18n的注解,如果有就設置'I18N_ATTRIBUTE'到request中并退出攔截器,如果沒有就繼續(xù)。
再判斷進入攔截的類上有沒有I18n的注解,如果有就設置'I18N_ATTRIBUTE'到request中并退出攔截器,如果沒有就繼續(xù)。
最后假如方法和類上都沒有I18n的注解,那我們可以根據(jù)Controller名自動設置指定的國際化文件,比如'UserController'那么就會去找'user'的國際化文件。
現(xiàn)在我們再運行一下看看效果,看到每個鏈接都顯示的他們對應的國際化信息里的內容。




最后
剛才完成了我們整個國際化增強的基本功能,最后我把全部代碼整理了一下,并且整合了bootstrap4來展示了一下功能的實現(xiàn)效果。




詳細的代碼可以看我Github上Spring-Boot-I18n-Pro的代碼
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持腳本之家。
- Spring Boot實現(xiàn)通用的接口參數(shù)校驗
- SpringBoot如何優(yōu)雅的處理校驗參數(shù)的方法
- 詳解如何在Spring Boot項目使用參數(shù)校驗
- Spring boot進行參數(shù)校驗的方法實例詳解
- Spring Boot 參數(shù)校驗的具體實現(xiàn)方式
- 詳解SpringBoot中的參數(shù)校驗(項目實戰(zhàn))
- springboot+thymeleaf國際化之LocaleResolver接口的示例
- SpringBoot實現(xiàn)國際化過程詳解
- Spring Boot Thymeleaf實現(xiàn)國際化的方法詳解
- Spring Boot REST國際化的實現(xiàn)代碼
- SpringBoot參數(shù)校驗與國際化使用教程
相關文章
Java Fluent Mybatis實戰(zhàn)之構建項目與代碼生成篇上
Java中常用的ORM框架主要是mybatis, hibernate, JPA等框架。國內又以Mybatis用的多,基于mybatis上的增強框架,又有mybatis plus和TK mybatis等。今天我們介紹一個新的mybatis增強框架 fluent mybatis2021-10-10
細數(shù)java中Long與Integer比較容易犯的錯誤總結
下面小編就為大家?guī)硪黄殧?shù)java中Long與Integer比較容易犯的錯誤總結。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-01-01
IntelliJ IDEA Project窗口的一些設置詳解
這篇文章主要介紹了IntelliJ IDEA Project窗口的一些設置詳解,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-08-08
JAVA中時間戳與LocalDateTime互相轉換代碼例子
最近在編碼過程中遇到將時間戳轉化為 LocalDateTime,所以這里給總結下,這篇文章主要給大家介紹了關于JAVA中時間戳與LocalDateTime互相轉換的相關資料,需要的朋友可以參考下2023-11-11

