Spring Boot 動態自訂i18n訊息messages.properties
範例。
範例環境:
- Java 8
- Spring Boot 2.2.1.RELEASE
- Spring Data JPA
- H2 database
- Lombok
- Maven
本篇重點在於系統啟動後會先從資料庫取得對應的i18n訊息內容並更新到ReloadableResourceBundleMessageSource
的Properties
。使用者可透過API新增及修改資料庫及多國語言訊息。可用的多國語言是資料庫與messages.properties
的聯集。
建立Spring Boot專案(參考IntelliJ IDEA,Eclipse STS)。
Spring Boot預設使用ResourceBundleMessageSource
作為i18n訊息的配置,但這邊改用自訂的ReloadableResourceBundleMessageSource
bean來取得messages.properties
檔的訊息才能在程式執行期間(runtime)動態更新properties。
專案的application.properties
,僅配置應用程式路徑與port,所以應用程式本機路徑為http://localhost:8080/demo
application.properteis
#context path
server.servlet.context-path=/demo
#port
server.port=8080
i18n多語系訊息預設檔messages.properties
,英文訊息檔message_en_US
及中文訊息檔messages_zh_TW
。
messages.properties
demo.message=old message
demo.hello-world=Hello World
messages_en_US.properties
demo.message=old message
demo.hello-world=Hello World
messages_zh_TW.properties
demo.message=舊訊息
demo.hello-world=哈囉世界
在src/main/resources/
新增data.sql
內容如下,此為H2資料庫的初始內容,系統啟動時會自動執行以下script並將資料增加到對應的資料表。
data.sql
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.message', '一個訊息');
INSERT INTO MESSAGE (ID, LANGUAGE_ID, KEY, MESSAGE) VALUES (2, 2, 'demo.message', 'a message');
INSERT INTO MESSAGE (ID, LANGUAGE_ID, KEY, MESSAGE) VALUES (3, 1, 'demo.hello-world', '您好世界');
INSERT INTO MESSAGE (ID, LANGUAGE_ID, KEY, MESSAGE) VALUES (4, 2, 'demo.hello-world', 'ZA WARUDO');
Language
為語言設定檔entity類。
Language
package com.abc.demo.entity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
public class Language {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true)
private String tag;
}
Message
為多國語言訊息設定檔entity類。
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;
}
實作一個繼承ReloadableResourceBundleMessageSource
的類別來呼叫取得Properties
的方法,例如下面的DemoReloadableResourceBundleMessageSource
。
DemoReloadableResourceBundleMessageSource
package com.abc.demo.i18n;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.stereotype.Component;
import java.util.Locale;
import java.util.Properties;
import java.util.Set;
import java.util.stream.Collectors;
@Component
public class DemoReloadableResourceBundleMessageSource extends ReloadableResourceBundleMessageSource {
public Properties getProperties(Locale locale) {
return getMergedProperties(locale).getProperties();
}
public void updateProperty(Locale locale, String key, String value) {
Properties properties = getProperties(locale);
properties.setProperty(key, value);
}
}
在Spring Boot配置類(本範例設在@SpringBootApplication
類)設定DemoReloadableResourceBundleMessageSource
為MessageSource
的bean。
注意setBasename()
的值前面要加上classpath:
才會從classpath(e.g. src/main/resources/
)讀取properties檔,且不用加副檔名.properties
。
DemoApplication
package com.abc.demo;
import com.abc.demo.i18n.DemoReloadableResourceBundleMessageSource;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import java.nio.charset.StandardCharsets;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
@Bean
public MessageSource messageSource() {
DemoReloadableResourceBundleMessageSource messageSource = new DemoReloadableResourceBundleMessageSource();
messageSource.setDefaultEncoding(StandardCharsets.UTF_8.name());
messageSource.setBasename("classpath:messages");
return messageSource;
}
}
定義一個系統啟動完成事件ApplicationReadyEvent
的監聽類別DemoApplicationStartedEventListener
。在onApplicationEvent()
讀取資料庫設定的i18n訊息來更新MessageSource
的properties內容。
DemoApplicationStartedEventListener
package com.abc.demo.listener;
import com.abc.demo.service.MessageService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
@Component
public class DemoApplicationStartedEventListener implements ApplicationListener {
@Autowired
private MessageService messageService;
@Override
public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) {
messageService.updateAllMessagePropertiesFromDatabase();
}
}
MessageService
的工作為更新資料庫及properties的內容。
MessageService
package com.abc.demo.service;
import com.abc.demo.controller.req.KeyValue;
import com.abc.demo.controller.req.UpdateMessageDto;
import com.abc.demo.entity.Language;
import com.abc.demo.i18n.DemoReloadableResourceBundleMessageSource;
import com.abc.demo.repository.LanguageRepository;
import com.abc.demo.repository.MessageRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Locale;
import java.util.Set;
@Service
public class MessageService {
@Autowired
private DemoReloadableResourceBundleMessageSource messageSource;
@Autowired
private LanguageRepository languageRepository;
@Autowired
private MessageRepository messageRepository;
public List<KeyValue<String, String>> getProperties(Locale locale) {
Properties properties = messageSource.getProperties(locale);
return properties.entrySet().stream().map((k) -> new KeyValue<>((String) k.getKey(), (String) k.getValue()))
.sorted(Comparator.comparing(KeyValue::getKey))
.collect(Collectors.toList());
}
public void updateAllMessagePropertiesFromDatabase() {
messageRepository.findAll().forEach(message -> {
Locale locale = languageRepository.findById(message.getLanguageId())
.map(language -> Locale.forLanguageTag(language.getTag()))
.orElseThrow(AssertionError::new);
messageSource.updateProperty(locale, message.getKey(), message.getMessage());
});
}
public void updateMessageProperties(UpdateMessageDto updateMessageDto) {
languageRepository.findByTag(updateMessageDto.getLanguageTag())
.ifPresent(language ->
updateMessageDto.getKeyValueList().forEach(
keyValue -> updateMessageProperty(language, keyValue)));
}
public void updateMessageProperty(Language language, KeyValue<String, String> keyValue) {
messageRepository.findByLanguageIdAndKey(language.getId(), keyValue.getKey())
.ifPresent(message -> {
message.setMessage(keyValue.getValue());
messageRepository.save(message);
messageSource.updateProperty(
Locale.forLanguageTag(language.getTag()),
keyValue.getKey(), keyValue.getValue());
});
}
}
Controller DemoController
定義API來測試properites值的更新效果。
呼叫/message
印出properties值;
呼叫/message/update
更新properties的內容。
DemoController
package com.abc.demo.controller;
import com.abc.demo.controller.req.UpdateMessageDto;
import com.abc.demo.i18n.DemoReloadableResourceBundleMessageSource;
import com.abc.demo.service.MessageService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Set;
@RestController
public class DemoController {
@Autowired
private DemoReloadableResourceBundleMessageSource messageSource;
@Autowired
private MessageService messageService;
@GetMapping("/message")
public void message() {
String s1 = messageSource.getMessage("demo.message", null, Locale.forLanguageTag("en-US"));
System.out.println(s1);
String s2 = messageSource.getMessage("demo.message", null, Locale.forLanguageTag("zh-TW"));
System.out.println(s2);
String s3 = messageSource.getMessage("demo.hello-world", null, Locale.forLanguageTag("en-US"));
System.out.println(s3);
String s4 = messageSource.getMessage("demo.hello-world", null, Locale.forLanguageTag("zh-TW"));
System.out.println(s4);
}
@GetMapping("/message/{languageTag}")
public List<String> getPropertiesKeyList(@PathVariable String languageTag) {
return messageService.getProperties(Locale.forLanguageTag(languageTag));
}
@PostMapping("/message/update")
public void updateMessage(@RequestBody UpdateMessageDto updateMessageDto) {
messageService.updateMessageProperties(updateMessageDto);
}
}
完整程式碼請參考github。
沒有留言:
張貼留言