說(shuō)起國(guó)際化,真的是老生常談了。后端有各種i18n的依賴組件,springboot本身也支持i18n的設(shè)置,前端vue也有i18n的設(shè)置,這些常規(guī)操作就不提了,大家可以去搜索其他博客,寫的都很詳細(xì)。
本篇博客主要寫的是業(yè)務(wù)內(nèi)容國(guó)際化。舉一個(gè)最常用最簡(jiǎn)單的例子,學(xué)生選課,課程有"語(yǔ)文","數(shù)學(xué)","英語(yǔ)"。這個(gè)課程也是一張業(yè)務(wù)表,隨著課程的增多數(shù)據(jù)是逐漸增多的。一個(gè)學(xué)生要查看自己選擇的課程時(shí),如何根據(jù)語(yǔ)言進(jìn)行國(guó)際化的反顯"數(shù)學(xué)"還是"mathematics"。
最開始我拿到這個(gè)需求的時(shí)候,很撓頭,怎么辦,難得不是把這個(gè)需求做出來(lái),這個(gè)需求實(shí)現(xiàn)得方式很多:
- 多建冗余字段,把”數(shù)學(xué)“和”mathematics“都存到表里,這樣有明顯得缺點(diǎn),語(yǔ)言增多時(shí)需要一直在表里加字段。
- 建一張code、language、value的對(duì)應(yīng)關(guān)系,查詢數(shù)據(jù)的時(shí)候根據(jù)code和language進(jìn)行value的匹配,這種缺陷也很明顯,業(yè)務(wù)侵入性很強(qiáng)。
我要做的事情是讓業(yè)務(wù)開發(fā)人員在無(wú)感知的情況下或侵入性很小的情況下把需求實(shí)現(xiàn)。提到侵入性小,大家很容易聯(lián)想到切面編程AOP。我個(gè)人認(rèn)為AOP最好用的地方就是能拿到自定義注解,通過(guò)在java類或者java方法上增加注解,在切面獲取引入的東西并將我們相要的東西織入。
靈感一來(lái),我們就開干。
一、建表,并存儲(chǔ)基礎(chǔ)數(shù)據(jù)
表的作用是能將各種code對(duì)應(yīng)的各種語(yǔ)言的各種value進(jìn)行匹配,建表比寫在配置文件的好處是顯而易見的,因?yàn)槲覀冏龅氖菢I(yè)務(wù)內(nèi)容的國(guó)際化,而不是定死的幾個(gè)值得國(guó)際化,我們需要根據(jù)業(yè)務(wù)動(dòng)態(tài)得調(diào)整內(nèi)容。這個(gè)表的數(shù)據(jù)可以開一個(gè)接口,業(yè)務(wù)數(shù)據(jù)發(fā)生變化時(shí),可以直接調(diào)用這個(gè)接口,對(duì)表中數(shù)據(jù)進(jìn)行更新。
表結(jié)構(gòu)如下:
?LANGUAGE_ID 主鍵
LANGUAGE_KEY 存在業(yè)務(wù)表中得業(yè)務(wù)標(biāo)識(shí)
LANGUAGE 語(yǔ)言標(biāo)識(shí)
LANGUAGE_VALUE 國(guó)際化后的值
MODEL 模塊名稱,主要防止KEY重復(fù),同一個(gè)key在不同的業(yè)務(wù)中代表的含義不同。
以上面選課為例,該表存放的值為
1?course math?en?mathematics
2?course math?zh-CN 數(shù)學(xué)
二、獲取表中數(shù)據(jù)放入緩存
數(shù)據(jù)咱們都有了,怎么把數(shù)據(jù)拿出來(lái)用呢,每次查庫(kù)?肯定不現(xiàn)實(shí),我們應(yīng)該提前把準(zhǔn)備好,放在緩存中,誰(shuí)想用直接取。緩存有多種方式。我們做jvm和redis兩種,讓大家做選擇,追求效率就用jvm緩存,不求效率就用redis,對(duì)本身服務(wù)影響小一些。
1、首先定義實(shí)體類
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
/**
* 系統(tǒng)語(yǔ)言ResultDTO
*/
@Getter
@Setter
@ToString
public class SysLanguageConfig {
private Long languageId;
private String model;
private String languageKey;
private String language;
private String languageValue;
private long currentPage;
private long pageSize;
}
2、獲取數(shù)據(jù)并緩存的配置類
package com.cnhtc.hdf.wf.common.i18n;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.xxl.job.core.context.XxlJobHelper;
import com.xxl.job.core.handler.annotation.XxlJob;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisStringCommands;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.types.Expiration;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StopWatch;
import java.util.*;
import java.util.concurrent.*;
@Configuration
@EnableFeignClients(clients = {SysLanguageConfigService.class})
@Slf4j
public class LanguageCahceConfigration {
public static ConcurrentHashMap<String, String> localCacheMap = new ConcurrentHashMap<>(); //本地存儲(chǔ)緩存的map
/**
* 存儲(chǔ)redis 熱點(diǎn)數(shù)據(jù)
*/
public static ConcurrentHashMap<String, String> redisHotspotCacheMap = new ConcurrentHashMap<>();
public final static String CACHE_KEY_JOIN_SYMBOL = "_";
private static Boolean i18n;
@Value("${i18nPageSize: 5000}")
private Integer i18nPageSize;
@Value("${i18nEnableInitDataParallel: false}")
private Boolean i18nEnableInitDataParallel;
/**
* 是否開啟redis熱點(diǎn)數(shù)據(jù)緩存,默認(rèn)不開啟
*/
private static Boolean i18nEnableRedisHotspotCache;
/**
* 開啟緩存模式
* local MAP
* redis
*/
private static String i18nEnableCacheMode;
private final static String CACHE_MODE = "local";
/**
* 多少個(gè)元素拆分一個(gè)List
*/
private final Integer splitListSize = 10;
/**
* 批量插入 條數(shù)
*/
private final Integer REDIS_BATCH_SAVE_SIZE = 5000;
/**
* 失效時(shí)間
*/
private final long EXPIRE_SECONDS = 3600 * 1000;
@Autowired
private SysLanguageConfigService sysLanguageConfigService;
@Bean
public SysLanguageConfigServiceFallback sysLanguageConfigServiceFallback() {
return new SysLanguageConfigServiceFallback();
}
public LanguageCahceConfigration() {
System.out.println("------------------- 加載 LanguageCahceConfigration ----------------------------------");
}
@Scheduled(initialDelay = 1000, fixedRateString = "${i18nScheduledFixedRate:3600000}")
public void setLanguageCacheMap() {
if (i18n) {
if (!CACHE_MODE.equals(i18nEnableCacheMode)) {
return;
}
CopyOnWriteArrayList<SysLanguageConfig> allList = new CopyOnWriteArrayList<>();
StopWatch sw = new StopWatch();
try {
sw.start("數(shù)據(jù)查詢");
if (i18nEnableInitDataParallel) {
this.selectDataCompletableFuture(allList);
} else {
this.selectData(allList);
}
sw.stop();
} catch (Exception e) {
e.printStackTrace();
allList.clear();
}
log.debug("allList size = {}", allList.size());
sw.start("本地緩存");
localCacheMap.clear();
localCacheMap.putAll(this.getCacheDataMap(allList));
sw.stop();
log.warn("初始化i18n 緩存耗時(shí) , {}", sw.prettyPrint());
log.warn("初始化i18n 緩存總耗時(shí) , {}", sw.getTotalTimeSeconds());
}
}
/**
* 循環(huán)查詢數(shù)據(jù)
*
* @param allList 數(shù)據(jù)集合
*/
private void selectData(CopyOnWriteArrayList<SysLanguageConfig> allList) {
int page = 1;
boolean isContinue = false;
do {
SysLanguageConfig sysLanguageConfig = new SysLanguageConfig();
sysLanguageConfig.setCurrentPage(page);
sysLanguageConfig.setPageSize(i18nPageSize);
Page<SysLanguageConfig> result = sysLanguageConfigService.getAll(sysLanguageConfig);
if (result != null && !CollectionUtils.isEmpty(result.getRecords())) {
allList.addAll(result.getRecords());
if (result.getPages() > page) {
isContinue = true;
page = page + 1;
} else {
isContinue = false;
}
}
} while (isContinue);
}
/**
* 異步分頁(yè)查詢數(shù)據(jù)
*
* @param allList 數(shù)據(jù)集合
* @throws Exception 異常
*/
private void selectDataCompletableFuture(CopyOnWriteArrayList<SysLanguageConfig> allList) throws Exception {
Page<SysLanguageConfig> result = this.getData();
if (result != null && result.getPages() > 0) {
allList.addAll(result.getRecords());
if (result.getPages() > 1) {
ForkJoinPool pool = new ForkJoinPool();
List<Integer> pageList = new ArrayList<>();
for (int i = 2; i <= result.getPages(); i++) {
pageList.add(i);
}
List<List<Integer>> partition = Lists.partition(pageList, splitListSize);
for (List<Integer> pages : partition) {
List<CompletableFuture<Void>> futureList = new ArrayList<>();
for (Integer page : pages) {
SysLanguageConfig param = new SysLanguageConfig();
param.setCurrentPage(page);
param.setPageSize(i18nPageSize);
CompletableFuture<Void> future = CompletableFuture.runAsync(() ->
allList.addAll(sysLanguageConfigService.getAll(param).getRecords()), pool);
futureList.add(future);
}
CompletableFuture<Void> allSources = CompletableFuture.allOf(futureList.toArray(new CompletableFuture[futureList.size()]));
allSources.get();
}
}
}
}
/**
* 獲取數(shù)據(jù)
* @return Page<SysLanguageConfig>
*/
private Page<SysLanguageConfig> getData(){
SysLanguageConfig sysLanguageConfig = new SysLanguageConfig();
sysLanguageConfig.setCurrentPage(1);
sysLanguageConfig.setPageSize(i18nPageSize);
return sysLanguageConfigService.getAll(sysLanguageConfig);
}
/**
* 批量插入并設(shè)置 失效時(shí)間,但是性能慢
*
* @param map 數(shù)據(jù)
*/
private void redisPipelineInsert(ConcurrentHashMap<String, String> map) {
StringRedisTemplate stringRedisTemplate = ApplicationContextUtil.getBean(StringRedisTemplate.class);
RedisSerializer<String> serializer = stringRedisTemplate.getStringSerializer();
stringRedisTemplate.executePipelined(new RedisCallback<String>() {
@Override
public String doInRedis(RedisConnection connection) throws DataAccessException {
map.forEach((key, value) -> {
connection.set(serializer.serialize(key), serializer.serialize(value), Expiration.seconds(EXPIRE_SECONDS), RedisStringCommands.SetOption.UPSERT);
});
return null;
}
}, serializer);
}
/**
* 批量插入后 異步設(shè)置失效時(shí)間
*
* @param map 數(shù)據(jù)
*/
//@Async
public void setExpire(ConcurrentHashMap<String, String> map) {
StringRedisTemplate stringRedisTemplate = ApplicationContextUtil.getBean(StringRedisTemplate.class);
map.forEach((k, v) -> stringRedisTemplate.expire(k, EXPIRE_SECONDS, TimeUnit.SECONDS));
}
/**
* 刷新redis緩存
*/
@XxlJob("i18nRefreshRedisCache")
public void refreshRedisCache() {
XxlJobHelper.log("回調(diào)任務(wù)開始");
if (i18n) {
if (CACHE_MODE.equals(i18nEnableCacheMode)) {
log.error("i18n國(guó)際化配置本地緩存,請(qǐng)勿用redis刷新緩存");
}
CopyOnWriteArrayList<SysLanguageConfig> allList = new CopyOnWriteArrayList<>();
StopWatch sw = new StopWatch();
try {
sw.start("數(shù)據(jù)查詢");
if (i18nEnableInitDataParallel) {
this.selectDataCompletableFuture(allList);
} else {
this.selectData(allList);
}
sw.stop();
} catch (Exception e) {
e.printStackTrace();
allList.clear();
}
sw.start("redis緩存");
StringRedisTemplate stringRedisTemplate = ApplicationContextUtil.getBean(StringRedisTemplate.class);
if (ObjectUtils.isEmpty(stringRedisTemplate)) {
throw new BaseException(ErrorEnum.NOTHROWABLE_ERROR, "StringRedisTemplate is null");
}
redisHotspotCacheMap.clear();
ConcurrentHashMap<String, String> cacheDataMap = this.getCacheDataMap(allList);
List<Map<String, String>> maps = splitMap(cacheDataMap, REDIS_BATCH_SAVE_SIZE);
// multiSet 批量插入,key值存在會(huì)覆蓋原值
maps.forEach(data -> stringRedisTemplate.opsForValue().multiSet(data));
sw.stop();
log.warn("初始化i18n redis緩存耗時(shí) , {}", sw.prettyPrint());
log.warn("初始化i18n redis緩存總耗時(shí) , {}", sw.getTotalTimeSeconds());
}
XxlJobHelper.log("回調(diào)任務(wù)結(jié)束");
}
/**
* 刷新local緩存
*/
//@XxlJob("i18nRefreshLocalCache")
public ResponseDTO refreshLocalCache() {
if (CACHE_MODE.equals(i18nEnableCacheMode)) {
this.setLanguageCacheMap();
return new ResponseDTO(SysErrEnum.SUCCESS);
}
return new ResponseDTO(SysErrEnum.ERROR.code(), "i18n國(guó)際化配置Redis緩存,請(qǐng)勿用本地刷新緩存");
}
/**
* 獲取緩存數(shù)據(jù)
*
* @param allList
* @return
*/
private ConcurrentHashMap<String, String> getCacheDataMap(List<SysLanguageConfig> allList) {
ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
allList.parallelStream().forEach(config -> map.put(config.getModel() + CACHE_KEY_JOIN_SYMBOL + config.getLanguageKey() + CACHE_KEY_JOIN_SYMBOL + config.getLanguage(), config.getLanguageValue()));
log.warn("map size:{}", map.size());
return map;
}
/**
* 獲取緩存數(shù)據(jù)值
*
* @param key key
* @return value
*/
public static String getCacheValueByKey(String key) {
if (i18n) {
String value;
if (CACHE_MODE.equals(i18nEnableCacheMode)) {
value = localCacheMap.get(key);
log.debug("多語(yǔ)言轉(zhuǎn)換:本地緩存數(shù)量 = {}, key = {}", +localCacheMap.values().size(), key);
} else {
if (i18nEnableRedisHotspotCache) {
if (redisHotspotCacheMap.containsKey(key)) {
value = redisHotspotCacheMap.get(key);
} else {
StringRedisTemplate stringRedisTemplate = ApplicationContextUtil.getBean(StringRedisTemplate.class);
value = stringRedisTemplate.opsForValue().get(key);
if (StringUtils.isNotBlank(value)) {
// 緩存熱點(diǎn)數(shù)據(jù)
redisHotspotCacheMap.put(key, value);
}
}
} else {
StringRedisTemplate stringRedisTemplate = ApplicationContextUtil.getBean(StringRedisTemplate.class);
value = stringRedisTemplate.opsForValue().get(key);
}
}
return value;
}
return null;
}
@Value("${i18nEnableCacheMode: local}")
private void setI18nEnableCacheMode(String i18nEnableCacheMode) {
LanguageCahceConfigration.i18nEnableCacheMode = i18nEnableCacheMode;
}
@Value("${i18nEnableRedisHotspotCache: false}")
private void setI18nEnableRedisHotspotCache(Boolean i18nEnableRedisHotspotCache) {
LanguageCahceConfigration.i18nEnableRedisHotspotCache = i18nEnableRedisHotspotCache;
}
@Value("${i18n: false}")
private void setI18n(Boolean i18n) {
LanguageCahceConfigration.i18n = i18n;
}
/**
* Map拆分 (指定分組大小)
*
* @param map Map
* @param chunkSize 每個(gè)分組的大小 (>=1)
* @param <K> Key
* @param <V> Value
* @return 子Map列表
*/
private <K, V> List<Map<K, V>> splitMap(Map<K, V> map, int chunkSize) {
if (Objects.isNull(map) || map.isEmpty() || chunkSize < 1) {
//空map或者分組大小<1,無(wú)法拆分
return Collections.emptyList();
}
int mapSize = map.size(); //鍵值對(duì)總數(shù)
int groupSize = mapSize / chunkSize + (mapSize % chunkSize == 0 ? 0 : 1); //計(jì)算分組個(gè)數(shù)
List<Map<K, V>> list = Lists.newArrayListWithCapacity(groupSize); //子Map列表
if (chunkSize >= mapSize) { //只能分1組的情況
list.add(map);
return list;
}
int count = 0; //每個(gè)分組的組內(nèi)計(jì)數(shù)
Map<K, V> subMap = Maps.newHashMapWithExpectedSize(chunkSize); //子Map
for (Map.Entry<K, V> entry : map.entrySet()) {
if (count < chunkSize) {
//給每個(gè)分組放chunkSize個(gè)鍵值對(duì),最后一個(gè)分組可能會(huì)裝不滿
subMap.put(entry.getKey(), entry.getValue());
count++; //組內(nèi)計(jì)數(shù)+1
} else {
//結(jié)束上一個(gè)分組
list.add(subMap); //當(dāng)前分組裝滿了->加入列表
//開始下一個(gè)分組
subMap = Maps.newHashMapWithExpectedSize(chunkSize); //新的分組
subMap.put(entry.getKey(), entry.getValue()); //添加當(dāng)前鍵值對(duì)
count = 1; //組內(nèi)計(jì)數(shù)重置為1
}
}
list.add(subMap); //添加最后一個(gè)分組
return list;
}
}
整段代碼其中區(qū)分了本地緩存、redis緩存等等,還有就是查剛才數(shù)據(jù)庫(kù)表里得數(shù)據(jù),因?yàn)槲覀儾庞昧宋⒎?wù)得架構(gòu),所以獲取數(shù)據(jù)得部分是通過(guò)feign的方式獲取的,大家可以替換成自己的方法。另外,開啟redis緩存的部分可以取舍,沒必要這么完善,保留一種即可。本地緩存的定時(shí)任務(wù)是springboot的,redis的定時(shí)任務(wù)是xxl-job的,這些技術(shù)棧都可以替換。
其中最重要的一點(diǎn),redis比本地緩存慢很多,100條數(shù)據(jù)的國(guó)際化反顯,速度會(huì)差20倍。為什么差怎么多,接下來(lái)就到關(guān)鍵內(nèi)容了。
三、注解定義
注解定義的意義就是在序列化的時(shí)候,能通過(guò)注解拿到切入點(diǎn)并獲取注解的內(nèi)容
import java.lang.annotation.*;
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
@Documented
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@JsonSerialize(
using = I18nSerializer.class
)
public @interface I18n {
String model() default "common";
String language() default "";
String key() default "";
}
四、基于JsonSerializer的序列化處理,進(jìn)行國(guó)際化轉(zhuǎn)換
大家現(xiàn)在都在用springboot的restController,也就是說(shuō),前后端分離之后,前后端的交互就是json,在controller返回的內(nèi)容其實(shí)就是一個(gè)實(shí)體對(duì)象或者集合,那這個(gè)實(shí)體對(duì)象或者集合是怎么轉(zhuǎn)換成json的,就是通過(guò)springboot中引入的jackson來(lái)實(shí)現(xiàn)的,具體實(shí)現(xiàn)原理不多說(shuō)。
我們只需要知道,寫一個(gè)子類,來(lái)繼承JsonSerializer和實(shí)現(xiàn)ContextualSerializer就能實(shí)現(xiàn)序列化的時(shí)候進(jìn)行織入操作。
其中l(wèi)anguage是通過(guò)header從前端傳遞過(guò)來(lái)的。文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-492518.html
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.ContextualSerializer;
import com.stdp.hdf.wf.common.core.constants.Constants;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Map;
import java.util.Objects;
@Slf4j
public class I18nSerializer extends JsonSerializer<String> implements ContextualSerializer {
private String model;
private String language;
private String key;
public I18nSerializer(String model, String language, String key) {
this.model = model;
this.language = language;
this.key = key;
}
public I18nSerializer() {
}
@Override
public void serialize(String s, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
String requestLanguage = null;
String mapkey = s;
if (StringUtils.isBlank(language)) {
requestLanguage = getLanguage();
} else {
requestLanguage = language;
}
if (StringUtils.isNotBlank(requestLanguage)) {
if (StringUtils.isNotBlank(key)) {
Object o = jsonGenerator.getCurrentValue();
mapkey = getPropertyValue(o, key).toString();
}
String keyString = model + LanguageCahceConfigration.CACHE_KEY_JOIN_SYMBOL + mapkey + LanguageCahceConfigration.CACHE_KEY_JOIN_SYMBOL + requestLanguage;
String keyName = LanguageCahceConfigration.getCacheValueByKey(keyString);
if (StringUtils.isBlank(keyName)) {
keyName = s;
}
jsonGenerator.writeString(keyName);
} else {
jsonGenerator.writeString(s);
}
}
@Override
public JsonSerializer<?> createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) throws JsonMappingException {
if (beanProperty != null) { // 為空直接跳過(guò)
if (Objects.equals(beanProperty.getType().getRawClass(), String.class)) { // 非 String 類直接跳過(guò)
I18n i18n = beanProperty.getAnnotation(I18n.class);
if (i18n == null) {
i18n = beanProperty.getContextAnnotation(I18n.class);
}
if (i18n != null) { // 如果能得到注解,就將注解的 value 傳入 I18nSerializer
return new I18nSerializer(i18n.model(), i18n.language(), i18n.key());
}
}
return serializerProvider.findValueSerializer(beanProperty.getType(), beanProperty);
}
return serializerProvider.findNullValueSerializer(beanProperty);
}
public String getLanguage() {
//直接從request中獲取language信息
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
if (requestAttributes == null) {
return null;
}
HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
return request.getHeader(Constants.LANGUAGE);
}
public Object getPropertyValue(Object t, String objProperty) {
Map<String, String> objMap = null;
try {
objMap = BeanUtils.describe(t);
if (objMap.get(objProperty) != null) {
return objMap.get(objProperty);
}
return "";
} catch (Exception e) {
e.printStackTrace();
}
return "";
}
}
五、使用
還是以選課為例,返回的json信息,CourseName自動(dòng)就轉(zhuǎn)成了對(duì)應(yīng)的語(yǔ)言。文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-492518.html
@Getter
@Setter
@ToString
public Course implements Serializable {
private String courseCode; //課程編號(hào)
@I18n(model = "course",key = "courseCode")
private String courseName; //課程名稱
}
到了這里,關(guān)于基于spring boot的JsonSerializer 業(yè)務(wù)內(nèi)容國(guó)際化的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!