AdSense

網頁

2020/6/30

Spring Boot i18n response message

Spring Boot回應代碼訊息多國語言(i18n)範例。

範例環境:

  • Java 8
  • Spring Boot 2.2.1.RELEASE
  • Spring Data JPA
  • H2 database
  • Lombok
  • Maven

流程大致如下。當API回傳結果時,透過攔截器攔截回應主體並從中取得回應代碼至資料庫[訊息設定檔]尋找對應的語言訊息,如果資料庫沒有設定該代碼的語言訊息則使用i18n訊息檔案的內容。


建立Spring Boot專案(參考IntelliJ IDEAEclipse STS)

專案的application.properites的預設本地訊息為zh-TW

application.properteis

#context path
server.servlet.context-path=/demo
#port
server.port=8080

demo.default.language-tag=zh-TW

多語系預設訊息messages.properties及中文訊息messages_zh_TW

messages.properties

demo.message=MessageSource auto config
demo.message.args=A message with args, arg_0={0}, arg_1={1}

demo.res.state.success=Success
demo.res.state.fail=Fail
demo.res.state.error=Error
demo.res.state.no-connection=No connection
demo.res.state.unknown=Unknown

messages_zh_TW.properties

demo.message=MessageSource自動配置
demo.message.args=帶參數的訊息,參數0={0}, 參數1={1}

demo.res.state.success=執行成功
demo.res.state.fail=執行失敗
demo.res.state.error=執行錯誤
demo.res.state.no-connection=沒有連線
demo.res.state.unknown=未知

錯誤代碼列舉ResState,第一個參數code為代碼,第二個參數messageKey為取得i18n訊息的key。

ResState

package com.abc.demo.controller.dto.res;

import lombok.Getter;

import java.util.Optional;

public enum ResState {

    SUCCESS("0000", "demo.res.state.success"),
    FAIL("0001", "demo.res.state.fail"),
    ERROR("9000", "demo.res.state.error"),
    NO_CONNECTION("9001", "demo.res.state.no-connection"),
    UNKNOWN("9999", "demo.res.state.unknown");

    @Getter
    private String code;
    @Getter
    private String messageKey;

    ResState(String code, String messageKey) {
        this.code = code;
        this.messageKey = messageKey;
    }

    public static Optional<ResState> getResStateByCode(String code) {
        for (ResState resState : ResState.values()) {
            if(resState.code.equals(code)) {
                return Optional.of(resState);
            }
        }
        return Optional.empty();
    }

}

對映語言訊息資料表LANGUAGEMESSAGE的實體類別如下。

Language

package com.abc.demo.entity;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
public class Language {

    @Id
    @GeneratedValue(strategy= GenerationType.IDENTITY)
    private Long id;

    private String tag;
}

Message

package com.abc.demo.entity;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
public class Message {

    @Id
    @GeneratedValue(strategy= GenerationType.IDENTITY)
    private Long id;

    private Long languageId;

    private String key;

    private String message;

}

系統啟動時H2資料庫的對LANGUAGEMESSAGE的初始資料。一個LANGUAGE對多個MESSAGE,兩者為One-to-Many關係。

INSERT INTO LANGUAGE (ID, TAG) VALUES (1, 'zh-TW');
INSERT INTO LANGUAGE (ID, TAG) VALUES (2, 'en-US');

INSERT INTO MESSAGE (ID, LANGUAGE_ID, KEY, MESSAGE) VALUES (1, 1, 'demo.res.state.no-connection', '無連線');
INSERT INTO MESSAGE (ID, LANGUAGE_ID, KEY, MESSAGE) VALUES (2, 2, 'demo.res.state.no-connection', 'No connection');

存取實體類的repository。

LanguageRepository

package com.abc.demo.repository;

import com.abc.demo.entity.Language;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.Optional;

@Repository
public interface LanguageRepository extends JpaRepository<Language, Long> {

    Optional<Language> findByTag(String tag);

}

MessageRepository

package com.abc.demo.repository;

import com.abc.demo.entity.Message;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.Optional;

@Repository
public interface MessageRepository extends JpaRepository<Message, Long> {

    Optional<Message> findByLanguageIdAndKey(Long languageId, String key);

}

API回應物件DemoResponse

DemoResponse

package com.abc.demo.controller.dto.res;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;

@Data
@AllArgsConstructor
@Builder
public class DemoResponse {

    private String message;
    private String code;

    public static DemoResponse state(ResState resState) {
        return DemoResponse.builder()
                .code(resState.getCode())
                .build();
    }

}

Controller DemoController。定義一個API,並用網址參數控制使用的語系。

DemoController

package com.abc.demo.controller;

import com.abc.demo.controller.dto.res.DemoResponse;
import com.abc.demo.controller.dto.res.ResState;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpSession;

@RestController
public class DemoController {

    @GetMapping("/message/{languageTag}")
    public DemoResponse message(HttpSession httpSession, @PathVariable String languageTag) {
        httpSession.setAttribute("languageTag", languageTag);

        return DemoResponse.state(ResState.SUCCESS);

    }
}

攔截API回傳結果的攔截器DemoResponseBodyAdvice。要在類別名稱前加上@CongtrollerAdvice並實作ResponseBodyAdvice才有作用。

beforeBodyWrite()中取得Controller API回傳的DemoResponse物件的回應代碼,並呼叫MessageServcie.getMessage()取得代碼對映的語系訊息。

DemoResponseBodyAdvice

package com.abc.demo.interceptor;

import com.abc.demo.controller.dto.res.DemoResponse;
import com.abc.demo.controller.dto.res.ResState;
import com.abc.demo.service.MessageService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

@ControllerAdvice
public class DemoResponseBodyAdvice implements ResponseBodyAdvice<DemoResponse> {

    @Autowired
    private MessageService messageService;

    @Override
    public boolean supports(
            MethodParameter methodParameter,
            Class<? extends HttpMessageConverter<?>> aClass) {

        return methodParameter.getParameterType().equals(DemoResponse.class);
    }

    @Override
    public DemoResponse beforeBodyWrite(
            DemoResponse demoResponse,
            MethodParameter methodParameter,
            MediaType mediaType,
            Class<? extends HttpMessageConverter<?>> aClass,
            ServerHttpRequest serverHttpRequest,
            ServerHttpResponse serverHttpResponse) {

        String code = demoResponse.getCode();

        String localeMessage = messageService.getMessage(
                ResState.getResStateByCode(code).orElse(ResState.UNKNOWN));

        demoResponse.setMessage(localeMessage);

        return demoResponse;
    }
}

MessageServcie用來依API傳入的語系取得資料庫對應的訊息,若無相對的訊息則從i18n訊息檔取得。

MessageService

package com.abc.demo.service;

import com.abc.demo.controller.dto.res.ResState;
import com.abc.demo.entity.Language;
import com.abc.demo.entity.Message;
import com.abc.demo.repository.LanguageRepository;
import com.abc.demo.repository.MessageRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.MessageSource;
import org.springframework.stereotype.Service;

import javax.servlet.http.HttpSession;
import java.util.Locale;

@Service
public class MessageService {

    @Value("${demo.default.language-tag}")
    private String defaultLauageTag;

    @Autowired
    private MessageSource messageSource;

    @Autowired
    private HttpSession httpSession;

    @Autowired
    private LanguageRepository languageRepository;

    @Autowired
    private MessageRepository messageRepository;

    public String getMessage(ResState resState) {
        String languageTag = (String) httpSession.getAttribute("languageTag");

        Long languageId = languageRepository.findByTag(languageTag)
                .map(Language::getId).orElse(0L);

        if (languageId == 0) {
            return messageSource.getMessage(resState.getMessageKey(), null, Locale.forLanguageTag(defaultLauageTag));
        }

        return messageRepository.findByLanguageIdAndKey(languageId, resState.getMessageKey())
                .map(Message::getMessage)
                .orElseGet(() ->
                        messageSource.getMessage(resState.getMessageKey(),
                        null,
                        Locale.forLanguageTag(languageTag)));
    }

}

測試API並帶入不同的語系及回傳結果如下。

GET | http://localhost:8080/demo/message/zh-TW

{
    "message": "執行成功",
    "code": "0000"
}

GET | http://localhost:8080/demo/message/en-US

{
    "message": "Success",
    "code": "0000"
}

GET | ttp://localhost:8080/demo/message/wryyyyyy

{
    "message": "執行成功",
    "code": "0000"
}

完整程式碼請參考github


沒有留言:

AdSense