前言
公司將項目由struts2轉(zhuǎn)到springmvc了,由于公司業(yè)務(wù)是境外服務(wù),所以對國際化功能需求很高。struts2自帶的國際化功能相對springmvc來說更加完善,不過spring很大的特性就是可定定制化性強,所以在公司項目移植的到springmvc的時候增加了其國際化的功能。特此整理記錄并且完善了一下。
本文主要實現(xiàn)的功能:
從文件夾中直接加載多個國際化文件后臺設(shè)置前端頁面顯示國際化信息的文件利用攔截器和注解自動設(shè)置前端頁面顯示國際化信息的文件
注:本文不詳細介紹怎么配置國際化,區(qū)域解析器等。
實現(xiàn)
國際化項目初始化
先創(chuàng)建一個基本的spring-boot+thymeleaf+國際化信息(message.properties)項目,如果有需要可以從我的github下載。
簡單看一下項目的目錄和文件
其中i18napplication.java設(shè)置了一個cookielocaleresolver
,采用cookie來控制國際化的語言。還設(shè)置一個localechangeinterceptor
攔截器來攔截國際化語言的變化。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
@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è)置存儲的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中寫了什么:
1
2
3
4
5
6
7
8
9
10
|
<!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)中設(shè)置了端口為9090。
由于瀏覽器默認的語言是中文,所以他默認會去messages_zh_cn.properties中找,如果沒有就會去messages.properties中找國際化詞。
然后我們在瀏覽器中輸入http://localhost:9090/hello?locale=en_us
,語言就會切到英文。同樣的如果url后參數(shù)設(shè)置為locale=zh_ch
,語言就會切到中文。
從文件夾中直接加載多個國際化文件
在我們hello.html頁面中,只有'i18n_page'和'hello'兩個國際化信息,然而在實際項目中肯定不會只有幾個國際化信息那么少,通常都是成千上百個的,那我們肯定不能把這么多的國際化信息都放在messages.properties
一個文件中,通常都是把國際化信息分類存放在幾個文件中。但是當(dāng)項目大了以后,這些國際化文件也會越來越多,這時候在application.properties
文件中一個個的去配置這個文件也是不方便的,所以現(xiàn)在我們實現(xiàn)一個功能自動加載制定目錄下所有的國際化文件。
繼承resourcebundlemessagesource
在項目下創(chuàng)建一個類繼承resourcebundlemessagesource
或者reloadableresourcebundlemessagesource
,起名為messageresourceextension
。并且注入到bean中起名為messagesource
,這里我們繼承resourcebundlemessagesource。
1
2
3
|
@component ( "messagesource" ) public class messageresourceextension extends resourcebundlemessagesource { } |
注意這里我們的component名字必須為'messagesource',因為在初始化applicationcontext
的時候,會查找bean名為'messagesource'的bean。這個過程在abstractapplicationcontext.java
中,我們看一下源代碼
1
2
3
4
5
6
7
8
9
10
11
12
|
/** * 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
中寫加載文件的方法了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
|
@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()); } } //設(shè)置父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); } } } /** * 把普通文件名轉(zhuǎn)換成國際化文件名 * * @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類被實例化之后自動調(diào)用init()
方法。這個方法獲取到basefolder
目錄下所有的國際化文件并設(shè)置到basenameset
中。并且設(shè)置一個parentmessagesource
,這會在找不到國際化信息的時候,調(diào)用父messagesource來查找國際化信息。 -
getallbasenames()
方法獲取到basefolder
的路徑,然后調(diào)用getallfile()
方法獲取到該目錄下所有的國際化文件的文件名。 -
getallfile()
遍歷目錄,如果是文件夾就繼續(xù)遍歷,如果是文件就調(diào)用geti18filename()
把文件名轉(zhuǎn)為'i18n/basename/‘格式的國際化資源名。
所以簡單來說就是在messageresourceextension
被實例化之后,把'i18n'文件夾下的資源文件的名字,加載到basenames
中。現(xiàn)在來看一下效果。
首先我們在application.properties文件中添加一個spring.messages.basefolder=i18n
,這會把'i18n'這個值賦值給messageresourceextension
中的basefolder
。
在啟動后看到控制臺里打印出了init信息,表示被@postconstruct
注解的init()方法已經(jīng)執(zhí)行。
然后我們再創(chuàng)建兩組國際化信息文件:'dashboard'和'merchant',里面分別只有一個國際化信息:'dashboard.hello'和'merchant.hello'。
之后再修改一下hello.html文件,然后訪問hello頁面。
1
2
3
4
5
6
7
8
|
... <body> <h1>國際化頁面!</h1> <p th:text= "#{hello}" ></p> <p th:text= "#{merchant.hello}" ></p> <p th:text= "#{dashboard.hello}" ></p> </body> ... |
可以看到網(wǎng)頁中加載了'message','dashboard'和'merchant'中的國際化信息,說明我們已經(jīng)成功一次性加載了'i18n'文件夾下的文件。
后臺設(shè)置前端頁面顯示國際化信息的文件
s剛才那一節(jié)我們成功加載了多個國際化文件并顯示出了他們的國際化信息。但是'dashboard.properties'中的國際化信息為'dashboard.hello'而'merchant.properties'中的是'merchant.hello',這樣每個都要寫一個前綴豈不是很麻煩,現(xiàn)在我想要在'dashboard'和'merchant'的國際化文件中都只寫'hello'但是顯示的是'dashboard'或'merchant'中的國際化信息。
在messageresourceextension
重寫resolvecodewithoutarguments
方法(如果有字符格式化的需求就重寫resolvecode
方法)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
@component ( "messagesource" ) public class messageresourceextension extends resourcebundlemessagesource { ... public static string i18n_attribute = "i18n_attribute" ; @override protected string resolvecodewithoutarguments(string code, locale locale) { // 獲取request中設(shè)置的指定國際化文件名 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'(等下再說這個在哪里設(shè)置),這個對應(yīng)我們想要顯示的國際化文件名,然后我們在basenameset
中查找該文件,再通過getresourcebundle
獲取到資源,最后再getstringornull
獲取到對應(yīng)的國際化信息。
現(xiàn)在我們到我們的hellocontroller
里加兩個方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
@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" ; } } |
看到我們在每個方法中都設(shè)置一個對應(yīng)的'i18n_attribute',這會在每次請求中設(shè)置對應(yīng)的國際化文件,然后在messageresourceextension
中獲取。
這時我們看一下我們的國際化文件,我們可以看到所有關(guān)鍵字都是'hello',但是信息卻不同。
同時新增兩個html文件分別是'dashboard.html'和'merchant.html',里面只有一個'hello'的國際化信息和用于區(qū)分的標(biāo)題。
1
2
3
4
5
|
<!-- 這是hello.html --> <body> <h1>國際化頁面!</h1> <p th:text= "#{hello}" ></p> </body> |
1
2
3
4
5
|
<!-- 這是dashboard.html --> <body> <h1>國際化頁面(dashboard)!</h1> <p th:text= "#{hello}" ></p> </body> |
1
2
3
4
5
|
<!-- 這是merchant.html --> <body> <h1>國際化頁面(merchant)!</h1> <p th:text= "#{hello}" ></p> </body> |
這時我們啟動項目看一下。
可以看到雖然在每個頁面的國際化詞都是'hello',但是我們在對應(yīng)的頁面顯示了我們想要顯示的信息。
利用攔截器和注解自動設(shè)置前端頁面顯示國際化信息的文件
雖然已經(jīng)可以指定對應(yīng)的國際化信息,但是這樣要在每個controller里的httpservletrequest中設(shè)置國際化文件實在太麻煩了,所以現(xiàn)在我們實現(xiàn)自動判定來顯示對應(yīng)的文件。
首先我們創(chuàng)建一個注解,這個注解可以放在類上或者方法上。
1
2
3
4
5
6
7
8
|
@target ({elementtype.type, elementtype.method}) @retention (retentionpolicy.runtime) public @interface i18n { /** * 國際化文件名 */ string value(); } |
然后我們把這個創(chuàng)建的i18n
注解放在剛才的controller方法中,為了顯示他的效果,我們再創(chuàng)建一個shopcontroller
和usercontroller
,同時也創(chuàng)建對應(yīng)的'shop'和'user'的國際化文件,內(nèi)容也都是一個'hello'。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
@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" ; } } |
1
2
3
4
5
6
7
8
|
@i18n ( "shop" ) @controller public class shopcontroller { @getmapping ( "shop" ) public string shop() { return "shop" ; } } |
1
2
3
4
5
6
7
|
@controller public class usercontroller { @getmapping ( "user" ) public string user() { return "user" ; } } |
我們把i18n
注解分別放在hellocontroller
下的dashboard
和merchant
方法下,和shopcontroller
類上。并且去除了原來dashboard
和merchant
方法下設(shè)置‘i18n_attribute'的語句。
準備工作都做好了,現(xiàn)在看看如何實現(xiàn)根據(jù)這些注解自動的指定國際化文件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
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中的國際化信息 req.removeattribute(messageresourceextension.i18n_attribute); return true ; } } |
簡單講解一下這個攔截器。
首先,如果request中已經(jīng)有'i18n_attribute',說明在controller的方法中指定設(shè)置了,就不再判斷。
然后判斷一下進入攔截器的方法上有沒有i18n
的注解,如果有就設(shè)置'i18n_attribute'到request中并退出攔截器,如果沒有就繼續(xù)。
再判斷進入攔截的類上有沒有i18n
的注解,如果有就設(shè)置'i18n_attribute'到request中并退出攔截器,如果沒有就繼續(xù)。
最后假如方法和類上都沒有i18n
的注解,那我們可以根據(jù)controller名自動設(shè)置指定的國際化文件,比如'usercontroller'那么就會去找'user'的國際化文件。
現(xiàn)在我們再運行一下看看效果,看到每個鏈接都顯示的他們對應(yīng)的國際化信息里的內(nèi)容。
最后
剛才完成了我們整個國際化增強的基本功能,最后我把全部代碼整理了一下,并且整合了bootstrap4來展示了一下功能的實現(xiàn)效果。
詳細的代碼可以看我github上spring-boot-i18n-pro的代碼
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持服務(wù)器之家。
原文鏈接:https://segmentfault.com/a/1190000014538512