網頁

2020/7/1

Spring Boot update i18n messages from database

Spring Boot 動態自訂i18n訊息messages.properties範例。

範例環境:

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

本篇重點在於系統啟動後會先從資料庫取得對應的i18n訊息內容並更新到ReloadableResourceBundleMessageSourceProperties。使用者可透過API新增及修改資料庫及多國語言訊息。可用的多國語言是資料庫與messages.properties的聯集。


建立Spring Boot專案(參考IntelliJ IDEAEclipse 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類)設定DemoReloadableResourceBundleMessageSourceMessageSource的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


沒有留言:

張貼留言