【Springboot採坑日記】Springboot+thymeleaf整合i18n的配置過程及注意事項

鏡雲兮發表於2020-10-09

【Springboot採坑日記】Springboot+thymeleaf整合i18n過程及注意事項

Springboot+thymeleaf+i18n的配置方式

thymeleaf整合

匯入thymeleaf依賴:

		<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
            <version>2.3.3.RELEASE</version>
        </dependency>

配置thymeleaf的yml檔案

spring:
  thymeleaf:
    cache: false # 將thymeleaf快取關閉
    encoding: UTF-8

然後就可以將值傳入model中,由前端html直接展示
Controller

@Controller
public class TestController {

    @RequestMapping("test")
    public String indexHandler(Model model, HttpServletRequest request, HttpSession session){
        model.addAttribute("welcome","hello world");
        model.addAttribute("student",new Test("zhangsan",80));
        model.addAttribute("gender","male");
        List<Test> students = new ArrayList<>();
        students.add(new Test("張三",13));
        students.add(new Test("李四",25));
        students.add(new Test("趙三",60));
        model.addAttribute("students",students);
        Map<String,Object> map = new HashMap<>();
        map.put("stu7",new Test("田七",27));
        map.put("stu8",new Test("劉八",28));
        map.put("stu9",new Test("鄭九",29));
        model.addAttribute("map",map);
        model.addAttribute("attrName","score");
        model.addAttribute("attrValue",99);
        model.addAttribute("welcome1","<h2>Thymeleaf,<br>I'm learning.</h2>");
        model.addAttribute("photo","email.png");
        model.addAttribute("elementId","reddiv");
        model.addAttribute("bgColor","red");
        model.addAttribute("isClose",false);
        model.addAttribute("school",null);
        List<String> cities = new ArrayList<>();
        model.addAttribute("cities",cities);
        request.setAttribute("req","reqValue");
        session.setAttribute("ses","sesValue");
        session.getServletContext().setAttribute("app","appValue");
        int[] nums = {1,2,3,4,5,6};
        model.addAttribute("nums",nums);
        model.addAttribute("today",new Date());
        model.addAttribute("cardId","684515202008258475");
        //這裡test表示thymeleaf所對應的test.html,不用寫副檔名
        return "test";
    }
 }

HTML

<!DOCTYPE html>
<html lang="en" xmlns:th="https://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Hello world</title>
</head>

<!--請求路徑:http://localhost:8888/test?name=zhangsan-->
<body>
<!--展示文字 類似jsp ${}-->
    <p th:text="${welcome}">這裡將顯示資料,但這些文字不顯示</p>
    <div th:text="${welcome}">這裡將顯示資料,但這些文字不顯示</div>
    <span th:text="${welcome}">這裡將顯示資料,但這些文字不顯示</span>
    <hr>

<!--展示類-->
    <p th:text="${student}" id="student">aaaa</p>
    <p th:text="${student.name}">vvvv</p>
    <p th:text="${student.age}">cccc</p>
    <hr>

    <p th:text="*{student}">aaaa</p>
    <p th:text="*{student.name}">vvvv</p>
    <p th:text="*{student.age}">cccc</p>
    <hr>
    <div th:object="${student}">
        <p th:text="*{name}">vvvv</p>
        <p th:text="*{age}">cccc</p>
    </div>
    <hr>

<!--頁面跳轉-->
    <a th:href="@{'http://localhost:8888/find/' + ${student.age}}">查詢</a>
    <a th:href="@{|http://localhost:8888/find/${student.age}|}">查詢2</a>
    <a th:href="@{|/find/${student.age}|}">查詢3,絕對路徑</a>
    <a th:href="@{|../find/${student.age}|}">查詢4,相對路徑</a>
    <a th:href="@{|/static/img/email.png|}">跳轉靜態資源</a>
    <hr>

<!--分支查詢,類似jsp <c:if/>-->
    <p th:if="${gender == 'male'}"></p>
    <p th:if="${gender != 'male'}"></p>

    <hr>
    <div th:switch="${student.age/10}">
        <p th:case="0">兒童</p>
        <p th:case="1">少年</p>
        <p th:case="2">青年</p>
        <p th:case="3">中年</p>
        <p th:case="4">青年</p>
        <p th:case="*">老年</p>
    </div>
    <hr>

<!--迴圈查詢,遍歷list,類似jsp <c:for:each>-->
    <p th:each="stu, xxx : ${students}">
        <!--        顯示當前遍歷物件是第幾個(從1開始計數)-->
        <span th:text="${xxx.count}"></span>
        <!--        顯示當前遍歷物件的索引(從1開始計數)-->
        <span th:text="${xxx.index}"></span>
        <!--        boolean值,判斷當前遍歷物件是不是第一個-->
        <span th:text="${xxx.first}"></span>
        <!--        boolean值,判斷當前遍歷物件是不是最後一個-->
        <span th:text="${xxx.last}"></span>
        <!--        boolean值,判斷當前遍歷物件是不是偶數-->
        <span th:text="${xxx.even}"></span>
        <!--        boolean值,判斷當前遍歷物件是不是奇數-->
        <span th:text="${xxx.odd}"></span>
        <!--        獲取屬性-->
        <span th:text="${stu.name}"></span>
        <span th:text="${stu.age}"></span>
    </p>
    <hr>
<p th:each="stu : ${students}">
    <!--        顯示當前遍歷物件是第幾個(從1開始計數)-->
    <span th:text="${stuStat.count}"></span>
    <!--        顯示當前遍歷物件的索引(從1開始計數)-->
    <span th:text="${stuStat.index}"></span>
    <!--        boolean值,判斷當前遍歷物件是不是第一個-->
    <span th:text="${stuStat.first}"></span>
    <!--        boolean值,判斷當前遍歷物件是不是最後一個-->
    <span th:text="${stuStat.last}"></span>
    <!--        boolean值,判斷當前遍歷物件是不是偶數-->
    <span th:text="${stuStat.even}"></span>
    <!--        boolean值,判斷當前遍歷物件是不是奇數-->
    <span th:text="${stuStat.odd}"></span>
    <!--        獲取屬性-->
    <span th:text="${stu.name}"></span>
    <span th:text="${stu.age}"></span>
</p>
<hr>

<!--迴圈遍歷,遍歷map-->
<div th:each="entry: ${map}">
    <p th:text="${entryStat.count}"></p>
    <!--?表示不為空-->
    <p th:text="${entry?.key}"></p>
    <p th:text="${entry.value}"></p>
    <p th:text="${entry.value.name}"></p>
    <p th:text="${entry.value.age}"></p>
</div>
<hr>

<!--html 相關的屬性使用-->
<div th:text="${welcome1}"></div>
<!--utext會解析動態標籤-->
<div th:utext="${welcome1}"></div>

<hr>
<input th:type="text" name="age" value="0">

<input type="text" th:name="${attrName}" th:value="${attrValue}">

<img src="/img/key.png">
<img th:src="|/img/${photo}|">
<hr>
<!--css-->
<!--內聯-->
th:inline的取值可以有四種:<br>
1)text: 標籤體中需要嵌入動態內容,預設值
2)javascript:js中需要嵌入動態內容
3)css:css中需要嵌入動態內容
4)none:不解析內嵌動態內容
<p th:inline="text">
    他的姓名是:[[${student.name}]]
</p>
<hr>
<p>
    她的姓名是:[[${student.name}]]
</p>
<hr>
<!--內嵌指令碼-->
<script th:inline="javascript" type="text/javascript">
    alert([[${student.name}]]);
</script>
<hr>
<!--內嵌CSS-->
<div id="reddiv">
    我的背景顏色為紅色
</div>
<style>
    #[[${elementId}]] {
        width: 500px;
        height: 100px;
        background: [[${bgColor}]];
    }
</style>
<hr>
    Thymeleaf 包含四種字面常量:文字、數字、布林值,及null<br>

<div>
    我愛你,<span th:text="中國"></span>
</div>
<div>
    3.14+6 = <span th:text="${3.14+6}"></span>
</div>
<div>
    Thymeleaf中的boolean常量為:true,false,TRUE,FALSE,True,False(不區分大小寫)
    <span th:if="${isClose == false}">
        歡迎光臨
    </span>
    <span th:if="${isClose} == false">
        歡迎光臨2
    </span>
</div>
<div>
    關於null值需要注意:<br>
    1)若物件未定義,其值為null<br>
    2)若物件定義了,但其值被指定為null,則值為null<br>
    3)若集合已經被定義,不為(null),擔其長度為0,此時的集合是不為null<br>
    school: <span th:if="${school} == null">物件值為null</span><br>
    cities: <span th:if="${cities} != null">集合不為空</span><br>
    country: <span th:if="${country} == null">物件未定義</span><br>
</div>
<div th:text="|我的姓名是:${student.name}|"></div>
<hr>

<!--API Sevlet內建物件-->
req = <div th:text="${#request.getAttribute('req')}"></div>
ses = <div th:text="${#session.getAttribute('ses')}"></div>
app = <div th:text="${#servletContext.getAttribute('app')}"></div>
contextPath = <div th:text="${#request.getContextPath()}"></div>
params = <div th:text="${#request.getParameter('name')}"></div>
<hr>
<!--thymeleaf內建物件-->
<div th:text="${#aggregates.sum(nums)}"></div>
<div th:text="${today}"></div>
<!--日期格式化-->
<div th:text="${#dates.format(today,'yyyy-MM-dd')}"></div>
<!--字串擷取-->
<div th:text="${#strings.substring(cardId,6,14)}"></div>

<!--全域性變數-->
version:<p th:text="${version}"></p>
</body>
</html>

Springboot+i18n整合

由於Springboot本身就整合了核心包core,因此可以直接通過定義解析器的形式完成i18n的配置
解析器定義方法如下:

@Configuration
@ComponentScan
public class I18nConfig extends AbstractLocaleContextResolver {

    public static final String LOCALE_SESSION_ATTRIBUTE_NAME = SessionLocaleResolver.class.getName()+".LOCALE";
    public static final String TIME_ZONE_SESSION_ATTRIBUTE_NAME = SessionLocaleResolver.class.getName() + ".TIME_ZONE";

    @Value("${spring.messages.basename}")
    public String[] basefilenames;

    @Bean(name = "localeResolver")
    public LocaleResolver localeResolverBean(){
        SessionLocaleResolver sessionLocaleResolver = new SessionLocaleResolver();
        //設定解析器的預設語言
        sessionLocaleResolver.setDefaultLocale(CommConsts.I18N_CONFIG);
        return sessionLocaleResolver;
    }

    @Bean(name = "messageSource")
    public ResourceBundleMessageSource resourceBundleMessageSource(){
        ResourceBundleMessageSource source = new ResourceBundleMessageSource();
        if(basefilenames != null ){
            for (int i = 0; i < basefilenames.length; i++){
                String basename = basefilenames[i];
                Assert.hasText(basename,"Basename must not be empty");
                this.basefilenames[i] = basename.trim();
            }
            source.setBasenames(basefilenames);
        }
        source.setDefaultEncoding("UTF-8");
        source.setUseCodeAsDefaultMessage(true);
        return source;
    }

    public void setLocale(HttpServletRequest request,HttpServletResponse response, Locale locale){
        this.setLocaleContext(request,response,locale != null ? new SimpleLocaleContext(locale) : null);
    }

    @Override
    public LocaleContext resolveLocaleContext(HttpServletRequest httpServletRequest) {
        return null;
    }

    @Override
    public void setLocaleContext(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, LocaleContext localeContext) {
        Locale locale = null;
        TimeZone timeZone = null;
        if(localeContext != null){
            locale = localeContext.getLocale();
            if(localeContext instanceof TimeZoneAwareLocaleContext){
                timeZone = ((TimeZoneAwareLocaleContext) localeContext).getTimeZone();
            }
        }
        WebUtils.setSessionAttribute(httpServletRequest,LOCALE_SESSION_ATTRIBUTE_NAME,locale);
        WebUtils.setSessionAttribute(httpServletRequest,TIME_ZONE_SESSION_ATTRIBUTE_NAME,timeZone);
    }

    @Override
    public Locale resolveLocale(HttpServletRequest httpServletRequest) {
        Locale locale = CommConsts.I18N_CONFIG;
        {
            String temp = httpServletRequest.getParameter("language");
            if(StringUtils.isNotEmpty(temp)){
                locale = new Locale(temp);
                return locale;
            }
        }
        Cookie[] cookies = httpServletRequest.getCookies();
        if(cookies != null){
            for(Cookie cookie : cookies){
                if(cookie.getName().equals("LONGi_Language")){
                    String temp = cookie.getValue();
                    if(StringUtils.isNotEmpty(temp)){
                        locale = new Locale(temp);
                    }
                    continue;
                }
            }
        }
        return locale;
    }
}

該配置類中:
localeResolverBean:用於每次解析bean時對解析器配置語言
ResourceBundleMessageSource :用於定義本地要掃描的i18n的包
setLocaleContext:將i18n的配置資訊繫結request
resolveLocale:是介面LocaleResolver的方法的重寫,動態繫結cookie中,實現屆時切換
CommConsts.I18N_CONFIG:自己定義的locale常量,大家可以根據自己需求制定
如:

public static final Locale I18N_CONFIG = Locale.ROOT;

其中basenames的配置在yaml檔案中

 spring:
 #i18n
  messages:
    basename: i18n/comm,i18n/customer,i18n/homepage,i18n/login,i18n/meter,i18n/prepay,i18n/system

最後,把解析器作為攔截器放入攔截器配置中

 @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(localeChangeInterceptor());
    }

    @Bean
    public LocaleChangeInterceptor localeChangeInterceptor(){
        LocaleChangeInterceptor lci = new LocaleChangeInterceptor();
        lci.setParamName("language");
        return lci;
    }

然後,再建立Resource生成相應的i18n目錄即可
i18n目錄結構
後臺有的時候需要呼叫i18n,因此需要一個MessageUtils類進行獲取

package top.powersys.system.utils;

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.MessageSource;
import org.springframework.context.NoSuchMessageException;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.stereotype.Component;

import java.text.MessageFormat;
import java.util.Locale;

@Component
@Slf4j
public class MessageUtils {

    private static MessageSource messageSource;

    public MessageUtils(MessageSource messageSource){
        MessageUtils.messageSource = messageSource;
    }

    /**
     * 獲取單個國際化翻譯值
     * @param msgkey
     * @param defaultMsg
     * @return
     */
    public static String get(String msgkey,String defaultMsg){
        try {
            return messageSource.getMessage(msgkey,null, LocaleContextHolder.getLocale());
        } catch (NoSuchMessageException e) {
            log.error(e.getMessage(),e);
            return defaultMsg;
        }
    }

    /**
     * 獲取多個引數取代翻譯
     * @param msgKey
     * @param defaultMsg
     * @param arg
     * @return
     */
    public static String get(String msgKey,String defaultMsg,Object... arg){
        try {
            msgKey = messageSource.getMessage(msgKey, arg, LocaleContextHolder.getLocale());
            return msgKey;
        } catch (NoSuchMessageException e) {
            log.error(e.getMessage(),e);
            return MessageFormat.format(defaultMsg, arg);
        }
    }

    /**
     * 指定語言獲得單個國際化翻譯
     * @param msgKey
     * @param defaultMsg
     * @param language
     * @return
     */
    public static String getByLanguage(String msgKey,String defaultMsg,String language){
        try {
            Locale locale = new Locale(language);
            msgKey = messageSource.getMessage(msgKey,null,locale);
            return msgKey;
        } catch (NoSuchMessageException e) {
            log.error(e.getMessage(),e);
            return defaultMsg;
        }
    }

    public static String getByLanguage(String msgKey,String defaultMsg,String language,Object... arg){
        try {
            Locale locale = new Locale(language);
            msgKey = messageSource.getMessage(msgKey,arg,locale);
            return msgKey;
        } catch (NoSuchMessageException e) {
            log.error(e.getMessage(),e);
            return MessageFormat.format(defaultMsg,arg);
        }
    }

}

上述配置完成後,只需要在i18n中寫入相應的值,即可得到其對應的i18n內容
如:
在這裡插入圖片描述
並且在前端引入

i18n test:
<cite th:text="#{test}"></cite>
<p th:text="#{test}"></p>

前端展示結果為
在這裡插入圖片描述
如果是對於後臺配置,如返回碼,如:
在這裡插入圖片描述
ErrorCode碼中改為

    USERNAME_OR_PASSWORD_ERROR(1001, MessageUtils.get("error_code.user_pwd_wrong",null))

即可以配置成功,至於result的封裝方法,大家可以參考網路上各位大佬的方式。
I18n的配置這裡是結合了
springboot i18n國際化後臺多種語言設定的方式

springboot+thymeleaf+i18n
兩位博主的配置方式後,筆者根據實際情況做了改進以後的配置結果。

需要注意的是

1、在html引用的時候,這種配置方式是在載入的時候將yml下所有的包都作為basename引入,如上述程式碼可知,伺服器一啟動載入,就會對messageSource初始化,執行resourceBundleMessageSource方法,然後迴圈對各bundle配置,所以不會區分是哪一個bundle,所以配置的時候,最好寫成xx.xx的形式。
2、在實際操作的過程中,MessageUtils的defaultMsg一般無值,不寫成null,也可以賦一個""表示空,在後端獲取全員配置的時候,也可以自己根據實際情況選擇字符集,但生產中基本用不到,因為基本都是全伺服器實現一種國際化。
3、完成配置返回的時候,要注意#{xxx}的寫法中,xxx表示的是i18n的xxx.properties具體的key,而不是檔名.key的格式,這裡即便生成目錄的時候就生成i18n/aaa/bbb/message的形式,寫html還是要寫成#{xxx}形式,而不是#{aaa.bbb.message.xxx}的形式,不管寫幾層目錄,配置的位置都只能在yml檔案中。這個是受限於messageSource的載入順序,resourceBundleMessageSource剛開始的初始化全部載入,後續使用並不會重新初始化messageSource,也不會在每次request連線來臨時再去指定指定bundle下的key,所以上述的配置不能指定指定的bundle,這點和jsp的配置不同,解決辦法有兩種:
1)每次獲取request連線的時候,再去指定bundle,要求每次連線都要指定bundle(不推薦,浪費資源,而且thymeleaf官方也沒有推薦)
2)一次性將所有的basenames載入完後,在寫bundle的時候,規範化寫,比如aaa.bbb的形式,避免key值重複
4、在用springboot做快速化開發的過程中,可以用assert代替throw new exception的形式,然後通過@ControllerAdivce中一起捕獲一起甩出,這裡就是採用了這種斷言的方法代替傳統的異常丟擲。

相關文章