最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

SpringBoot實(shí)現(xiàn)i18n國際化的兩種企業(yè)級方案

 更新時間:2026年01月13日 09:23:19   作者:J_liaty  
在全球化業(yè)務(wù)場景下,系統(tǒng)適配多語言已成為標(biāo)配需求,SpringBoot作為主流的Java開發(fā)框架,提供了完善的國際化,本文將從實(shí)戰(zhàn)角度出發(fā),完整講解兩種企業(yè)級i18n實(shí)現(xiàn)方案:基于配置文件的靜態(tài)實(shí)現(xiàn)和基于數(shù)據(jù)庫的動態(tài)實(shí)現(xiàn),需要的朋友可以參考下

前言

在全球化業(yè)務(wù)場景下,系統(tǒng)適配多語言已成為標(biāo)配需求。SpringBoot作為主流的Java開發(fā)框架,提供了完善的國際化(i18n,internationalization的縮寫,因i和n之間有18個字母得名)解決方案。本文將從實(shí)戰(zhàn)角度出發(fā),完整講解兩種企業(yè)級i18n實(shí)現(xiàn)方案:基于配置文件的靜態(tài)實(shí)現(xiàn)(適配簡體中文、繁體中文、英文)和基于數(shù)據(jù)庫的動態(tài)實(shí)現(xiàn)(支持運(yùn)行時修改語言配置),同時覆蓋校驗(yàn)注解國際化、性能優(yōu)化、常見問題排查等核心要點(diǎn),所有代碼均可直接落地到生產(chǎn)項(xiàng)目。

一、國際化基礎(chǔ)認(rèn)知

1.1 核心概念

i18n的核心目標(biāo)是讓系統(tǒng)在不修改代碼的前提下,通過配置適配不同語言和地區(qū)的使用習(xí)慣。SpringBoot中實(shí)現(xiàn)i18n的核心依賴是:

  • MessageSource:消息源接口,負(fù)責(zé)加載和解析多語言消息,默認(rèn)實(shí)現(xiàn)為ResourceBundleMessageSource(基于配置文件)。
  • Accept-Language:語言地區(qū)標(biāo)識,格式為語言代碼_國家/地區(qū)代碼,如:
    • 簡體中文:zh_CN
    • 繁體中文:zh_TW
    • 英文(美國):en_US
  • LocaleResolver:語言解析器,負(fù)責(zé)從請求中獲取/設(shè)置當(dāng)前Locale。
  • LocaleChangeInterceptor:語言切換攔截器,用于攔截請求參數(shù)實(shí)現(xiàn)語言動態(tài)切換。

1.2 核心原理

SpringBoot啟動時,MessageSource會加載指定路徑下的多語言配置文件;當(dāng)業(yè)務(wù)代碼獲取國際化消息時,框架會根據(jù)當(dāng)前Accept-Language從對應(yīng)配置文件/數(shù)據(jù)源中匹配消息鍵(Key),返回對應(yīng)的消息值(Value)。

二、方式一:基于配置文件的i18n實(shí)現(xiàn)

2.1 環(huán)境準(zhǔn)備

2.1.1 依賴配置

新建SpringBoot項(xiàng)目(推薦2.7.x或3.2.x),核心依賴僅需spring-boot-starter-web,無需額外依賴:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- 可選:簡化配置文件編寫(.yml) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
</dependencies>

2.1.2 目錄結(jié)構(gòu)

resources目錄下創(chuàng)建i18n文件夾,用于存放多語言配置文件,最終目錄結(jié)構(gòu):

resources/
├── application.yml          # 核心配置
└── i18n/                    # 國際化配置文件目錄
    ├── messages.properties  # 默認(rèn)配置(無語言標(biāo)識)
    ├── messages_zh_CN.properties  # 簡體中文
    ├── messages_zh_TW.properties  # 繁體中文
    └── messages_en_US.properties  # 英文

2.2 多語言配置文件編寫

2.2.1 命名規(guī)則

配置文件命名必須遵循basename_語言代碼_國家代碼.properties規(guī)則:

  • basename:自定義前綴(如messages),需在application.yml中配置。
  • 無語言標(biāo)識的messages.properties默認(rèn)配置,當(dāng)匹配不到指定Locale的配置時,會使用該文件內(nèi)容。

2.2.2 配置文件內(nèi)容

  1. 默認(rèn)配置(messages.properties):兜底使用,建議與默認(rèn)語言(簡體中文)保持一致
# 通用提示
common.submit=提交
common.cancel=取消
# 用戶相關(guān)
user.name=用戶名
user.age=年齡
# 校驗(yàn)提示
validate.required.id=主鍵不能為空
validate.required.name=姓名不能為空
  1. 簡體中文(messages_zh_CN.properties)
# 通用提示
common.submit=提交
common.cancel=取消
# 用戶相關(guān)
user.name=用戶名
user.age=年齡
# 校驗(yàn)提示
validate.required.id=主鍵不能為空
validate.required.name=姓名不能為空
  1. 繁體中文(messages_zh_TW.properties)
# 通用提示
common.submit=提交
common.cancel=取消
# 用戶相關(guān)
user.name=使用者名稱
user.age=年齡
# 校驗(yàn)提示
validate.required.id=主鍵不能為空
validate.required.name=姓名不能為空
  1. 英文(messages_en_US.properties)
# 通用提示
common.submit=Submit
common.cancel=Cancel
# 用戶相關(guān)
user.name=Username
user.age=Age
# 校驗(yàn)提示
validate.required.id=Primary key cannot be empty
validate.required.name=Name cannot be empty

注意:properties文件默認(rèn)編碼為ISO-8859-1,直接寫中文會亂碼!需將IDE的properties文件編碼設(shè)置為UTF-8(IDEA:Settings → File Encodings → Properties Files → 勾選Transparent native-to-ascii conversion)。

2.3 SpringBoot核心配置

application.yml中配置國際化相關(guān)參數(shù),指定配置文件路徑、默認(rèn)語言、編碼等:

spring:
  # 國際化配置
  messages:
    basename: i18n/messages  # 配置文件路徑(無需寫.properties后綴)
    encoding: UTF-8         # 解決中文亂碼
    fallback-to-system-locale: false  # 禁用系統(tǒng)語言回退
    default-locale: zh_CN    # 默認(rèn)語言:簡體中文
    cache-duration: 3600s   # 配置文件緩存時間(生產(chǎn)建議設(shè)置)
  # Web配置(可選,用于請求參數(shù)解析)
  web:
    locale: zh_CN

2.4 自定義語言解析器與攔截器

默認(rèn)情況下,SpringBoot僅支持從請求頭Accept-Language獲取Locale,為了方便通過請求參數(shù)(如?Accept-Language=en-US)切換語言,需自定義LocaleResolver并注冊攔截器。

2.4.1 自定義LocaleResolver

創(chuàng)建config/I18nConfig.java,實(shí)現(xiàn)LocaleResolver接口:

package com.example.i18n.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.i18n.SessionLocaleResolver;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Locale;

/**
 * 國際化核心配置類(修改為Header攔截語言)
 */
@Configuration
public class I18nConfig implements WebMvcConfigurer {

    /**
     * 注冊自定義LocaleResolver(基于Session存儲Locale)
     * 替代默認(rèn)的AcceptHeaderLocaleResolver
     */
    @Bean
    public LocaleResolver localeResolver() {
        SessionLocaleResolver resolver = new SessionLocaleResolver();
        // 設(shè)置默認(rèn)語言:簡體中文(與application.yml中保持一致)
        resolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE); // 默認(rèn):zh_CN
        return resolver;
    }

    /**
     * 自定義攔截器:從 Accept-Language Header 解析并設(shè)置 Locale
     */
    @Bean
    public HandlerInterceptor localeHeaderInterceptor(LocaleResolver localeResolver) {
        return new HandlerInterceptor() {
            @Override
            public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
                    throws Exception {
                // 1. 從請求頭中獲取語言標(biāo)識(自定義Header名:Accept-Language,可根據(jù)需求修改)
                String acceptLanguage = request.getHeader("Accept-Language");
                // 2. 設(shè)置默認(rèn)語言為空或者拋異常使用
                Locale locale = Locale.SIMPLIFIED_CHINESE; // 默認(rèn)語言
                // 3. 若Header中有值,則解析并設(shè)置Accept-Language;無值則使用默認(rèn)Accept-Language
                if (acceptLanguage != null && !acceptLanguage.isEmpty()) {
                    try {
                        // 取第一個語言項(xiàng)(如 "zh-CN,en;q=0.8" → "zh-CN")
                        String primary = acceptLanguage.split(",")[0].trim();
                        // Spring 工具類能正確解析 "zh-CN"、"en" 等格式
                        locale = StringUtils.parseLocale(primary);
                    } catch (Exception e) {
                        // 解析失敗則使用默認(rèn)語言,不拋異常
                    }
                }

                // 使用容器中真實(shí)的 LocaleResolver 實(shí)例設(shè)置 Locale(存入 Session)
                localeResolver.setLocale(request, response, locale);
                return true;
            }
        };
    }

    /**
     * 注冊攔截器到 Spring MVC 攔截器鏈
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(localeHeaderInterceptor(localeResolver()))
                .addPathPatterns("/**")
                .order(0); // 優(yōu)先級最高
    }
}

關(guān)鍵說明

  • SessionLocaleResolver:將Locale存儲在Session中,一次切換后,后續(xù)請求無需重復(fù)傳參。
  • localeHeaderInterceptor:攔截請求頭HeaderAccept-Language參數(shù),自動更新當(dāng)前Locale(如Accept-Language=zh-TW會切換為繁體中文)。

2.5 國際化消息使用示例

2.5.1 工具類封裝(推薦)

創(chuàng)建utils/I18nUtils.java,封裝獲取國際化消息的方法,簡化業(yè)務(wù)使用:

package com.example.i18n.utils;

import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.Locale;

/**
 * 國際化工具類
 */
@Component
public class I18nUtils {

    @Resource
    private MessageSource messageSource;

    /**
     * 獲取國際化消息(使用當(dāng)前Locale)
     * @param key 消息鍵
     * @return 消息值
     */
    public String getMessage(String key) {
        return getMessage(key, null, LocaleContextHolder.getLocale());
    }

    /**
     * 獲取國際化消息(帶參數(shù))
     * @param key 消息鍵
     * @param args 參數(shù)數(shù)組(如消息為"你好{0}",args=new Object[]{"張三"})
     * @return 消息值
     */
    public String getMessage(String key, Object[] args) {
        return getMessage(key, args, LocaleContextHolder.getLocale());
    }

    /**
     * 手動指定Locale獲取消息
     * @param key 消息鍵
     * @param args 參數(shù)數(shù)組
     * @param locale 語言標(biāo)識
     * @return 消息值
     */
    public String getMessage(String key, Object[] args, Locale locale) {
        try {
            // 從MessageSource中獲取消息,若未找到則返回key本身
            return messageSource.getMessage(key, args, locale);
        } catch (Exception e) {
            return key;
        }
    }
}

核心API說明

  • LocaleContextHolder.getLocale():獲取當(dāng)前線程的Locale(由LocaleResolver解析)。
  • messageSource.getMessage(key, args, locale):核心方法,參數(shù)說明:
    • key:消息鍵(如user.name)。
    • args:消息參數(shù)(用于替換消息中的占位符,如user.hello=你好{0})。
    • locale:指定語言標(biāo)識。

2.5.2 控制器使用示例

創(chuàng)建controller/I18nController.java,編寫接口測試國際化效果:

package com.example.i18n.controller;

import com.example.i18n.utils.I18nUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

/**
 * 國際化測試控制器
 */
@RestController
@RequestMapping("/api/i18n")
public class I18nController {

    @Resource
    private I18nUtils i18nUtils;

    /**
     * 測試基礎(chǔ)國際化消息
     * 請求頭添加:Accept-Language: en-US(或 zh-TW /zh-CN)
     * 訪問示例:
     * - 簡體中文:http://localhost:8080/api/i18n/basic
     * - 繁體中文:http://localhost:8080/api/i18n/basic
     * - 英文:http://localhost:8080/api/i18n/basic
     */
    @GetMapping("/basic")
    public Map<String, String> getBasicMessage() {
        Map<String, String> result = new HashMap<>();
        // 獲取當(dāng)前語言的消息
        result.put("user.name", i18nUtils.getMessage("user.name"));
        result.put("common.submit", i18nUtils.getMessage("common.submit"));
        result.put("validate.required.id", i18nUtils.getMessage("validate.required.id"));
        return result;
    }

    /**
     * 測試帶參數(shù)的國際化消息
     */
    @GetMapping("/with-params")
    public Map<String, String> getMessageWithParams() {
        Map<String, String> result = new HashMap<>();
        // 模擬帶參數(shù)的消息(需先在配置文件中添加:user.hello=你好{0},user.hello=你好{0}(繁),user.hello=Hello {0}(英))
        String helloMsg = i18nUtils.getMessage("user.hello", new Object[]{"張三"});
        result.put("user.hello", helloMsg);
        return result;
    }

    /**
     * 手動指定Locale獲取消息
     */
    @GetMapping("/manual-locale")
    public Map<String, String> getMessageByManualLocale() {
        Map<String, String> result = new HashMap<>();
        // 手動指定繁體中文
        result.put("zh_TW.user.name", i18nUtils.getMessage("user.name", null, Locale.TRADITIONAL_CHINESE));
        // 手動指定英文
        result.put("en_US.user.name", i18nUtils.getMessage("user.name", null, new Locale("en", "US")));
        return result;
    }
}

2.6 測試驗(yàn)證

啟動項(xiàng)目后,通過Postman/Browser訪問以下地址驗(yàn)證效果:

簡體中文:http://localhost:8080/api/i18n/basic?lang=zh_CN
返回:

{
  "user.name":"用戶名",
  "common.submit":"提交",
  "validate.required.id":"主鍵不能為空"
}

繁體中文:http://localhost:8080/api/i18n/basic?lang=zh_TW
返回:

{
  "user.name":"使用者名稱",
  "common.submit":"提交",
  "validate.required.id":"主鍵不能為空"
}

英文:http://localhost:8080/api/i18n/basic?lang=en_US
返回:

{
  "user.name":"Username",
  "common.submit":"Submit",
  "validate.required.id":"Primary key cannot be empty"
}

2.7 默認(rèn)語言切換說明

默認(rèn)語言的生效優(yōu)先級:

  1. SessionLocaleResolver中設(shè)置的setDefaultLocale()(代碼級)。
  2. application.ymlspring.messages.default-locale(配置級)。
  3. 系統(tǒng)默認(rèn)Locale(兜底)。

若需修改默認(rèn)語言為英文,只需調(diào)整兩處:

// 1. I18nConfig中
localeResolver.setDefaultLocale(Locale.US);

// 2. application.yml中
spring:
  messages:
    default-locale: en_US

三、方式二:基于數(shù)據(jù)庫的動態(tài)i18n實(shí)現(xiàn)

基于配置文件的方式存在明顯缺陷:修改消息需重啟服務(wù)?;跀?shù)據(jù)庫的實(shí)現(xiàn)可實(shí)現(xiàn)運(yùn)行時動態(tài)配置多語言消息,適合頻繁變更或大規(guī)模多語言場景。

3.1 設(shè)計思路

  1. 設(shè)計數(shù)據(jù)庫表存儲多語言消息(鍵、語言、值)。
  2. 自定義MessageSource實(shí)現(xiàn),重寫消息解析邏輯,從數(shù)據(jù)庫加載消息。
  3. 引入緩存(Caffeine)提升性能,避免頻繁查詢數(shù)據(jù)庫。
  4. 提供接口實(shí)現(xiàn)消息的新增/修改/刪除,支持動態(tài)刷新緩存。

3.2 數(shù)據(jù)庫表設(shè)計

3.2.1 建表語句(MySQL)

CREATE TABLE `sys_i18n_message` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主鍵ID',
  `message_key` varchar(100) NOT NULL COMMENT '消息鍵(全局唯一+語言)',
  `language` varchar(20) NOT NULL COMMENT '語言標(biāo)識(zh_CN/zh_TW/en_US)',
  `message_value` varchar(500) NOT NULL COMMENT '消息值',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '創(chuàng)建時間',
  `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間',
  `deleted` tinyint DEFAULT 0 COMMENT '刪除標(biāo)記(0-未刪,1-已刪)',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_key_language` (`message_key`,`language`) COMMENT '消息鍵+語言唯一約束'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='國際化消息表';

3.2.2 測試數(shù)據(jù)插入

INSERT INTO `sys_i18n_message` (`message_key`, `language`, `message_value`) VALUES
('user.name', 'zh_CN', '用戶名'),
('user.name', 'zh_TW', '使用者名稱'),
('user.name', 'en_US', 'Username'),
('common.submit', 'zh_CN', '提交'),
('common.submit', 'zh_TW', '提交'),
('common.submit', 'en_US', 'Submit'),
('validate.required.id', 'zh_CN', '主鍵不能為空'),
('validate.required.id', 'zh_TW', '主鍵不能為空'),
('validate.required.id', 'en_US', 'Primary key cannot be empty');

3.3 環(huán)境準(zhǔn)備

3.3.1 添加依賴

在原有依賴基礎(chǔ)上,添加數(shù)據(jù)庫相關(guān)依賴(以MyBatis-Plus為例):

<!-- 數(shù)據(jù)庫驅(qū)動 -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>
<!-- MyBatis-Plus(簡化CRUD) -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.3.1</version>
</dependency>
<!-- 緩存:Caffeine -->
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>3.1.8</version>
</dependency>
<!-- 連接池 -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.2.20</version>
</dependency>

3.3.2 數(shù)據(jù)庫配置

application.yml中添加數(shù)據(jù)庫配置:

spring:
  # 數(shù)據(jù)庫配置
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/i18n_demo?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password: 123456
# MyBatis-Plus配置
mybatis-plus:
  mapper-locations: classpath:mapper/**/*.xml
  type-aliases-package: com.example.i18n.entity
  configuration:
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

3.4 核心組件開發(fā)

3.4.1 實(shí)體類

創(chuàng)建entity/SysI18nMessage.java

package com.example.i18n.entity;

import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;

import java.time.LocalDateTime;

/**
 * 國際化消息實(shí)體
 */
@Data
@TableName("sys_i18n_message")
public class SysI18nMessage {

    /**
     * 主鍵ID
     */
    @TableId(type = IdType.AUTO)
    private Long id;

    /**
     * 消息鍵
     */
    @TableField("message_key")
    private String messageKey;

    /**
     * 語言標(biāo)識
     */
    @TableField("language")
    private String language;

    /**
     * 消息值
     */
    @TableField("message_value")
    private String messageValue;

    /**
     * 創(chuàng)建時間
     */
    @TableField(value = "create_time", fill = FieldFill.INSERT)
    private LocalDateTime createTime;

    /**
     * 更新時間
     */
    @TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;

    /**
     * 刪除標(biāo)記
     */
    @TableField("deleted")
    @TableLogic
    private Integer deleted;
}

3.4.2 Mapper接口

創(chuàng)建mapper/SysI18nMessageMapper.java

package com.example.i18n.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.i18n.entity.SysI18nMessage;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;

import java.util.List;

/**
 * 國際化消息Mapper
 */
public interface SysI18nMessageMapper extends BaseMapper<SysI18nMessage> {

    /**
     * 根據(jù)消息鍵和語言查詢消息
     */
    @Select("SELECT message_value FROM sys_i18n_message WHERE message_key = #{key} AND language = #{language} AND deleted = 0")
    String getMessageByKeyAndLanguage(@Param("key") String key, @Param("language") String language);

    /**
     * 查詢所有消息(用于預(yù)加載緩存)
     */
    @Select("SELECT message_key, language, message_value FROM sys_i18n_message WHERE deleted = 0")
    List<SysI18nMessage> listAllMessages();
}

3.4.3 Service層

創(chuàng)建service/SysI18nMessageService.java(接口):

package com.example.i18n.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.example.i18n.entity.SysI18nMessage;

import java.util.Map;

/**
 * 國際化消息服務(wù)
 */
public interface SysI18nMessageService extends IService<SysI18nMessage> {

    /**
     * 根據(jù)鍵和語言獲取消息
     */
    String getMessage(String key, String language);

    /**
     * 加載所有消息到緩存
     */
    Map<String, String> loadAllMessagesToCache();

    /**
     * 刷新緩存
     */
    void refreshCache();
}

創(chuàng)建service/impl/SysI18nMessageServiceImpl.java(實(shí)現(xiàn)類):

package com.example.i18n.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.i18n.entity.SysI18nMessage;
import com.example.i18n.mapper.SysI18nMessageMapper;
import com.example.i18n.service.SysI18nMessageService;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * 國際化消息服務(wù)實(shí)現(xiàn)
 */
@Service
public class SysI18nMessageServiceImpl extends ServiceImpl<SysI18nMessageMapper, SysI18nMessage> implements SysI18nMessageService {

    /**
     * 緩存Key規(guī)則:messageKey + "_" + language
     */
    private final Cache<String, String> i18nCache = Caffeine.newBuilder()
            .expireAfterWrite(1, TimeUnit.HOURS) // 1小時過期
            .maximumSize(10000) // 最大緩存10000條
            .build();

    /**
     * 項(xiàng)目啟動時預(yù)加載所有消息到緩存
     */
    @PostConstruct
    public void initCache() {
        loadAllMessagesToCache();
    }

    @Override
    public String getMessage(String key, String language) {
        // 構(gòu)造緩存Key
        String cacheKey = key + "_" + language;
        // 先查緩存,緩存未命中則查數(shù)據(jù)庫
        return i18nCache.get(cacheKey, k -> {
            String message = baseMapper.getMessageByKeyAndLanguage(key, language);
            // 數(shù)據(jù)庫未找到則返回key本身
            return message == null ? key : message;
        });
    }

    @Override
    public Map<String, String> loadAllMessagesToCache() {
        List<SysI18nMessage> messageList = baseMapper.listAllMessages();
        Map<String, String> messageMap = new HashMap<>();
        for (SysI18nMessage message : messageList) {
            String cacheKey = message.getMessageKey() + "_" + message.getLanguage();
            messageMap.put(cacheKey, message.getMessageValue());
        }
        // 將所有消息放入緩存
        i18nCache.putAll(messageMap);
        return messageMap;
    }

    @Override
    public void refreshCache() {
        // 清空緩存并重新加載
        i18nCache.invalidateAll();
        loadAllMessagesToCache();
    }
}

核心說明

  • @PostConstruct:項(xiàng)目啟動時執(zhí)行initCache(),預(yù)加載所有消息到緩存,提升首次訪問性能。
  • Caffeine緩存:設(shè)置1小時過期+最大容量,避免緩存膨脹;緩存Key為消息鍵_語言(如user.name_zh_CN)。
  • 緩存穿透處理:數(shù)據(jù)庫未找到消息時,返回消息鍵本身,避免緩存穿透。

3.4.4 自定義MessageSource

創(chuàng)建config/DbMessageSource.java,繼承AbstractMessageSource(Spring提供的MessageSource抽象實(shí)現(xiàn)):

package com.example.i18n.config;

import com.example.i18n.service.SysI18nMessageService;
import org.springframework.context.support.AbstractMessageSource;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.text.MessageFormat;
import java.util.Locale;

/**
 * 基于數(shù)據(jù)庫的MessageSource實(shí)現(xiàn)
 */
@Component
public class DbMessageSource extends AbstractMessageSource {

    @Resource
    private SysI18nMessageService sysI18nMessageService;

    /**
     * 核心方法:解析消息
     */
    @Override
    protected MessageFormat resolveCode(String code, Locale locale) {
        // 獲取語言標(biāo)識(如zh_CN)
        String language = locale.toString();
        // 從數(shù)據(jù)庫+緩存中獲取消息值
        String message = sysI18nMessageService.getMessage(code, language);
        // 若未找到,嘗試使用默認(rèn)語言(zh_CN)
        if (message.equals(code) && !language.equals("zh_CN")) {
            message = sysI18nMessageService.getMessage(code, "zh_CN");
        }
        // 封裝為MessageFormat(支持參數(shù)替換)
        return createMessageFormat(message, locale);
    }

    /**
     * 重載方法:直接返回字符串(簡化使用)
     */
    public String getMessage(String code, Locale locale) {
        return resolveCode(code, locale).format(null);
    }

    public String getMessage(String code, Object[] args, Locale locale) {
        return resolveCode(code, locale).format(args);
    }
}

3.4.5 替換默認(rèn)MessageSource

I18nConfig.java中注冊自定義的DbMessageSource,替換SpringBoot默認(rèn)的ResourceBundleMessageSource

/**
 * 注冊數(shù)據(jù)庫版MessageSource,優(yōu)先級高于默認(rèn)實(shí)現(xiàn)
 */
@Bean
@Primary // 標(biāo)記為首選Bean
public MessageSource messageSource(SysI18nMessageService sysI18nMessageService) {
    DbMessageSource messageSource = new DbMessageSource();
    // 設(shè)置默認(rèn)語言(與之前保持一致)
    messageSource.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
    // 設(shè)置編碼
    messageSource.setDefaultEncoding("UTF-8");
    return messageSource;
}

3.5 業(yè)務(wù)集成與測試

3.5.1 工具類適配

修改I18nUtils.java,注入自定義的DbMessageSource

// 替換原有的MessageSource為自定義的DbMessageSource
@Resource
private DbMessageSource messageSource;

// 其余方法無需修改,邏輯完全兼容

3.5.2 消息管理接口

創(chuàng)建controller/I18nManageController.java,提供消息的新增/修改/刷新緩存接口:

package com.example.i18n.controller;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.example.i18n.entity.SysI18nMessage;
import com.example.i18n.service.SysI18nMessageService;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import java.util.Map;

/**
 * 國際化消息管理接口(動態(tài)配置)
 */
@RestController
@RequestMapping("/api/i18n/manage")
public class I18nManageController {

    @Resource
    private SysI18nMessageService sysI18nMessageService;

    /**
     * 新增/修改國際化消息
     */
    @PostMapping("/save")
    public String saveMessage(@RequestBody SysI18nMessage message) {
        // 先刪除已存在的同Key+語言的消息
        LambdaQueryWrapper<SysI18nMessage> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(SysI18nMessage::getMessageKey, message.getMessageKey())
                .eq(SysI18nMessage::getLanguage, message.getLanguage());
        sysI18nMessageService.remove(wrapper);
        // 保存新消息
        sysI18nMessageService.save(message);
        // 刷新緩存
        sysI18nMessageService.refreshCache();
        return "操作成功";
    }

    /**
     * 刷新緩存
     */
    @GetMapping("/refresh-cache")
    public String refreshCache() {
        sysI18nMessageService.refreshCache();
        return "緩存刷新成功";
    }

    /**
     * 查詢所有消息
     */
    @GetMapping("/list-all")
    public Map<String, String> listAllMessages() {
        return sysI18nMessageService.loadAllMessagesToCache();
    }
}

3.5.3 測試驗(yàn)證

  1. 基礎(chǔ)消息查詢:訪問http://localhost:8080/api/i18n/basic?lang=en_US,返回數(shù)據(jù)庫中的英文消息。
  2. 動態(tài)修改消息
    • 調(diào)用POST接口http://localhost:8080/api/i18n/manage/save,傳入JSON:
{
  "messageKey": "user.name",
  "language": "en_US",
  "messageValue": "User Name"
}
  • 調(diào)用刷新緩存接口:http://localhost:8080/api/i18n/manage/refresh-cache。
  • 再次訪問基礎(chǔ)查詢接口,user.name會返回User Name(無需重啟服務(wù))。

四、進(jìn)階優(yōu)化措施

4.1 緩存優(yōu)化(數(shù)據(jù)庫方式)

  1. 多級緩存:結(jié)合Caffeine(本地緩存)+ Redis(分布式緩存),適配集群場景。
  2. 緩存預(yù)熱:項(xiàng)目啟動時預(yù)加載所有消息到緩存,避免首次訪問數(shù)據(jù)庫。
  3. 緩存主動失效:消息修改后立即刷新緩存,而非等待過期。
  4. 批量加載:分頁加載大量消息,避免一次性加載過多數(shù)據(jù)導(dǎo)致內(nèi)存溢出。

4.2 語言解析器增強(qiáng)

擴(kuò)展LocaleResolver,支持多維度語言解析(優(yōu)先級從高到低):

  1. 請求參數(shù)(lang)→ 2. Cookie → 3. 請求頭(Accept-Language)→ 4. Session → 5. 默認(rèn)語言。

示例代碼:

@Component
public class CustomLocaleResolver implements LocaleResolver {

    @Override
    public Locale resolveLocale(HttpServletRequest request) {
        // 1. 優(yōu)先從請求參數(shù)獲取
        String lang = request.getParameter("lang");
        if (StringUtils.hasText(lang)) {
            String[] split = lang.split("_");
            return new Locale(split[0], split[1]);
        }
        // 2. 從Cookie獲取
        Cookie[] cookies = request.getCookies();
        if (cookies != null) {
            for (Cookie cookie : cookies) {
                if ("lang".equals(cookie.getName())) {
                    String[] split = cookie.getValue().split("_");
                    return new Locale(split[0], split[1]);
                }
            }
        }
        // 3. 從請求頭獲取
        String acceptLanguage = request.getHeader("Accept-Language");
        if (StringUtils.hasText(acceptLanguage)) {
            return Locale.forLanguageTag(acceptLanguage.split(",")[0]);
        }
        // 4. 默認(rèn)語言
        return Locale.SIMPLIFIED_CHINESE;
    }

    @Override
    public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) {
        // 設(shè)置Cookie,有效期7天
        Cookie cookie = new Cookie("lang", locale.toString());
        cookie.setMaxAge(60 * 60 * 24 * 7);
        cookie.setPath("/");
        response.addCookie(cookie);
    }
}

4.3 動態(tài)刷新配置(配置文件方式)

使用Spring Cloud ConfigNacos實(shí)現(xiàn)配置文件的動態(tài)刷新,無需重啟服務(wù):

  1. 將多語言配置文件放到配置中心。
  2. 引入spring-cloud-starter-config依賴。
  3. 配置@RefreshScope,實(shí)現(xiàn)配置熱更新。

4.4 異常處理國際化

全局異常處理器中使用國際化工具類,返回多語言異常信息:

@RestControllerAdvice
public class GlobalExceptionHandler {

    @Resource
    private I18nUtils i18nUtils;

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, String>> handleValidationException(MethodArgumentNotValidException e) {
        Map<String, String> errors = new HashMap<>();
        e.getBindingResult().getFieldErrors().forEach(fieldError -> {
            // 獲取國際化后的校驗(yàn)提示
            String message = i18nUtils.getMessage(fieldError.getDefaultMessage());
            errors.put(fieldError.getField(), message);
        });
        return ResponseEntity.badRequest().body(errors);
    }
}

五、常見問題與解決方案

5.1 校驗(yàn)注解(@NotNull等)國際化適配

問題描述

直接使用@NotNull(message = "主鍵不能為空")是硬編碼,無法實(shí)現(xiàn)國際化。

解決方案

  1. 核心原理:JSR-303校驗(yàn)框架默認(rèn)讀取ValidationMessages.properties配置文件,可將校驗(yàn)消息鍵指向i18n配置。
  2. 實(shí)現(xiàn)步驟
    • 步驟1:在i18n配置文件/數(shù)據(jù)庫中添加校驗(yàn)消息鍵(如validate.required.id=主鍵不能為空)。
    • 步驟2:校驗(yàn)注解中使用{鍵名}引用國際化消息:
public class UserDTO {
    @NotNull(message = "{validate.required.id}")
    private Long id;

    @NotBlank(message = "{validate.required.name}")
    private String name;
    // 省略getter/setter
}
  • 步驟3:配置校驗(yàn)框架使用自定義的MessageSource:
@Bean
public Validator validator(MessageSource messageSource) {
    LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
    // 設(shè)置校驗(yàn)消息源為自定義的DbMessageSource/ResourceBundleMessageSource
    validator.setValidationMessageSource(messageSource);
    return validator;
}

@Override
public Validator getValidator() {
    return validator(messageSource);
}

5.2 配置文件中文亂碼

問題描述

properties文件中寫中文,讀取后顯示亂碼。

解決方案

  1. IDE配置:IDEA中設(shè)置File → Settings → Editor → File Encodings
    • Properties Files (*.properties):編碼設(shè)為UTF-8。
    • 勾選Transparent native-to-ascii conversion。
  2. 配置文件指定編碼:spring.messages.encoding=UTF-8。
  3. 手動轉(zhuǎn)碼:使用native2ascii工具將中文轉(zhuǎn)為ASCII編碼(不推薦)。

5.3 默認(rèn)語言不生效

問題描述

配置了默認(rèn)語言,但未傳參時仍使用系統(tǒng)語言。

解決方案

  1. 檢查LocaleResolver是否設(shè)置了setDefaultLocale()。
  2. 確認(rèn)application.ymlspring.messages.fallback-to-system-locale=false(禁用系統(tǒng)語言回退)。
  3. 檢查自定義LocaleResolverresolveLocale方法,默認(rèn)分支是否返回指定的默認(rèn)Locale。

5.4 數(shù)據(jù)庫方式性能問題

問題描述

高并發(fā)場景下,數(shù)據(jù)庫查詢頻繁,響應(yīng)慢。

解決方案

  1. 增加Caffeine本地緩存,設(shè)置合理的過期時間和最大容量。
  2. 集群場景下使用Redis分布式緩存,避免每個節(jié)點(diǎn)都查詢數(shù)據(jù)庫。
  3. 對熱點(diǎn)消息(如通用提示)進(jìn)行永久緩存,非熱點(diǎn)消息設(shè)置較短過期時間。
  4. 數(shù)據(jù)庫表添加索引(已在建表語句中添加uk_key_language唯一索引)。

5.5 動態(tài)修改語言后不生效(適配 Header 方式)

問題描述

傳參lang=en_US后,返回的仍為默認(rèn)語言。

解決方案

  1. 檢查自定義 Header 攔截器是否注冊到 SpringMVC 攔截器鏈,且攔截路徑包含目標(biāo)接口(需確保addPathPatterns(“/**”))。
  2. 確認(rèn) Header 攔截器中request.getHeader(“lang”)的 Header 名稱與實(shí)際請求一致(如前端傳的是Lang/LANG會導(dǎo)致讀取不到,HTTP Header 不區(qū)分大小寫,但建議統(tǒng)一小寫)。
  3. 檢查攔截器中 Locale 解析邏輯:
  4. 確認(rèn)lang的格式是否符合解析規(guī)則(如en_US是下劃線分隔,而非en-US);
  5. 檢查異常處理邏輯,若解析失敗是否回退到默認(rèn) Locale(避免解析異常導(dǎo)致 Locale 未設(shè)置)。
  6. 檢查LocaleResolver的setLocale方法是否正確實(shí)現(xiàn)(如SessionLocaleResolver需確保 Session
  7. 正常生效,無 Session 失效 / 隔離問題)。
  8. 排查是否存在攔截器執(zhí)行順序問題:確保語言攔截器優(yōu)先于其他業(yè)務(wù)攔截器執(zhí)行(可通過order()指定優(yōu)先級,如registry.addInterceptor(xxx).order(0))。

5.6 數(shù)據(jù)庫消息未找到時返回Key本身

問題描述

數(shù)據(jù)庫中未配置某個消息鍵,返回的是鍵名而非兜底消息。

解決方案

DbMessageSourceresolveCode方法中,增加兜底邏輯:

// 若未找到當(dāng)前語言的消息,嘗試默認(rèn)語言,仍未找到則返回兜底提示
if (message.equals(code)) {
    message = sysI18nMessageService.getMessage(code, "zh_CN");
    if (message.equals(code)) {
        message = "未找到對應(yīng)的提示信息:" + code;
    }
}

5.7 Header 中語言標(biāo)識格式錯誤導(dǎo)致切換失敗

問題描述

Header 傳入Accept-Language=en-US(中劃線分隔)或lang=english(非標(biāo)準(zhǔn)格式),語言切換不生效,始終返回默認(rèn)語言。

解決方案

  1. 攔截器中增加格式兼容邏輯,支持中劃線 / 下劃線兩種格式:
// 兼容en-US、zh-CN等中劃線格式
String langHeader = request.getHeader("Accept-Language").replace("-", "_");
  1. 增加語言標(biāo)識白名單校驗(yàn),僅允許合法的語言值:
// 定義合法語言列表
Set<String> validLangs = new HashSet<>(Arrays.asList("zh_CN", "zh_TW", "en_US"));
if (validLangs.contains(langHeader)) {
    // 正常解析
    String[] langParts = langHeader.split("_");
    Locale locale = new Locale(langParts[0], langParts[1]);
    localeResolver().setLocale(request, response, locale);
} else {
    // 非法值使用默認(rèn)語言
    localeResolver().setLocale(request, response, Locale.SIMPLIFIED_CHINESE);
}
  1. 前端規(guī)范:約定前端僅傳遞zh_CN/zh_TW/en_US三種格式,避免非法值。

5.8 跨域請求時 Header 中的 lang 參數(shù)丟失

問題描述

前后端分離項(xiàng)目中,前端跨域請求時攜帶Accept-Language Header,但后端無法讀取到該值,語言切換失效。

解決方案

  1. 配置跨域(CORS)允許自定義 Header:
@Configuration
public class CorsConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOriginPatterns("*") // 生產(chǎn)環(huán)境替換為具體域名
                .allowedMethods("GET", "POST", "PUT", "DELETE")
                .allowedHeaders("Accept-Language", "Content-Type") // 允許lang Header
                .exposedHeaders("Accept-Language") // 暴露lang Header(可選)
                .allowCredentials(true)
                .maxAge(3600);
    }
}
  1. 前端請求時確保攜帶Accept-Language Header 且跨域請求開啟withCredentials(若后端配置了allowCredentials=true):
// Axios示例
axios({
  url: "http://localhost:8080/api/i18n/basic",
  method: "GET",
  headers: {
    "Accept-Language": "en_US"
  },
  withCredentials: true // 關(guān)鍵:跨域攜帶Cookie/Session(SessionLocaleResolver依賴)
});

5.9 自定義 MessageSource 優(yōu)先級低于默認(rèn)實(shí)現(xiàn)導(dǎo)致校驗(yàn)注解國際化不生效

問題描述

校驗(yàn)注解中使用{validate.required.id}引用國際化鍵,但返回的仍是鍵名而非國際化值

解決方案

  1. 確保自定義的MessageSource(如DbMessageSource)添加@Primary注解,優(yōu)先級高于默認(rèn)的ResourceBundleMessageSource
@Bean
@Primary // 標(biāo)記為首選Bean
public MessageSource messageSource(SysI18nMessageService sysI18nMessageService) {
    DbMessageSource messageSource = new DbMessageSource();
    messageSource.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
    messageSource.setDefaultEncoding("UTF-8");
    return messageSource;
}
  1. 檢查校驗(yàn)器配置是否正確注入自定義MessageSource,而非默認(rèn)實(shí)現(xiàn):
// 確保注入的是自定義的MessageSource
@Resource
private MessageSource messageSource;

@Bean
public Validator validator() {
    LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
    validator.setValidationMessageSource(messageSource);
    return validator;
}
  1. 排查是否存在多個MessageSource Bean,導(dǎo)致 Spring 注入錯誤的實(shí)例。

六、總結(jié)

6.1 兩種方式對比

特性基于配置文件基于數(shù)據(jù)庫
靈活性低(需重啟服務(wù))高(運(yùn)行時動態(tài)修改)
性能高(內(nèi)存加載)中(需緩存優(yōu)化)
維護(hù)成本低(文件管理)高(需開發(fā)管理接口)
適用場景小型系統(tǒng)、消息變更少大型系統(tǒng)、多語言頻繁變更

6.2 核心要點(diǎn)回顧

  1. SpringBoot i18n的核心是MessageSourceLocaleResolver、LocaleChangeInterceptor三大組件。
  2. 配置文件方式需遵循命名規(guī)則,注意編碼問題;數(shù)據(jù)庫方式需自定義MessageSource并結(jié)合緩存優(yōu)化。
  3. 校驗(yàn)注解國際化需將message值設(shè)為{鍵名},并配置校驗(yàn)框架使用自定義MessageSource。
  4. 生產(chǎn)環(huán)境中,數(shù)據(jù)庫方式需做好緩存優(yōu)化,配置文件方式可結(jié)合配置中心實(shí)現(xiàn)動態(tài)刷新。

通過本文的兩種實(shí)現(xiàn)方案,你可以根據(jù)項(xiàng)目規(guī)模和需求選擇合適的國際化方式,同時規(guī)避常見問題,實(shí)現(xiàn)高效、穩(wěn)定的多語言適配。

以上就是SpringBoot實(shí)現(xiàn)i18n國際化的兩種企業(yè)級方案的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot實(shí)現(xiàn)i18n國際化的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • Java并發(fā)編程之阻塞隊列深入詳解

    Java并發(fā)編程之阻塞隊列深入詳解

    這篇文章主要介紹了詳解Java阻塞隊列(BlockingQueue)的實(shí)現(xiàn)原理,阻塞隊列是Java util.concurrent包下重要的數(shù)據(jù)結(jié)構(gòu),是一種特殊的隊列,需要的朋友可以參考下
    2021-10-10
  • 實(shí)例講解spring boot 多線程

    實(shí)例講解spring boot 多線程

    這篇文章主要介紹了spring boot 多線程的相關(guān)資料,文中示例代碼非常詳細(xì),幫助大家更好的理解和學(xué)習(xí),感興趣的朋友可以了解下
    2020-07-07
  • java獲取網(wǎng)絡(luò)圖片上傳到OSS的方法

    java獲取網(wǎng)絡(luò)圖片上傳到OSS的方法

    這篇文章主要為大家詳細(xì)介紹了java獲取網(wǎng)絡(luò)圖片上傳到OSS,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2018-10-10
  • 后端java壓縮圖片超詳細(xì)圖文教程

    后端java壓縮圖片超詳細(xì)圖文教程

    這篇文章主要給大家介紹了關(guān)于后端java壓縮圖片的相關(guān)資料,片壓縮是一種廣泛采用的技術(shù),它不僅能顯著減小文件大小,釋放更多存儲空間,還能提升圖片加載速度,避免長時間等待,需要的朋友可以參考下
    2024-04-04
  • SpringMVC 響應(yīng)數(shù)據(jù)和結(jié)果視圖從環(huán)境搭建到實(shí)戰(zhàn)全解析

    SpringMVC 響應(yīng)數(shù)據(jù)和結(jié)果視圖從環(huán)境搭建到實(shí)戰(zhàn)全解析

    SpringMVC中,Controller方法的返回值決定了響應(yīng)方式,本文詳細(xì)講解了SpringMVC的開發(fā)環(huán)境搭建、Controller方法返回值分類、轉(zhuǎn)發(fā)與重定向機(jī)制,以及JSON異步交互的實(shí)現(xiàn),本文介紹SpringMVC 響應(yīng)數(shù)據(jù)和結(jié)果視圖從環(huán)境搭建到實(shí)戰(zhàn)全解析,感興趣的朋友跟隨小編一起看看吧
    2025-11-11
  • 詳解Spring 基于 Aspect 注解的增強(qiáng)實(shí)現(xiàn)

    詳解Spring 基于 Aspect 注解的增強(qiáng)實(shí)現(xiàn)

    本篇文章主要介紹了詳解Spring 基于 Aspect 注解的增強(qiáng)實(shí)現(xiàn),非常具有實(shí)用價值,需要的朋友可以參考下
    2017-04-04
  • 詳解java中產(chǎn)生死鎖的原因及如何避免

    詳解java中產(chǎn)生死鎖的原因及如何避免

    這篇文章主要介紹了java中產(chǎn)生死鎖的原因及如何避免,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2019-04-04
  • java獲取一個文本文件的編碼(格式)信息

    java獲取一個文本文件的編碼(格式)信息

    這篇文章主要介紹了java獲取一個文本文件的編碼(格式)信息,文章圍繞主題展開詳細(xì)的內(nèi)容介紹,具有一定的參考價值,需要的小伙伴可以參考一下
    2022-09-09
  • SpringBoot @ConfigurationProperties使用詳解

    SpringBoot @ConfigurationProperties使用詳解

    這篇文章主要介紹了SpringBoot @ConfigurationProperties使用詳解,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2020-02-02
  • 詳解Java構(gòu)建樹結(jié)構(gòu)的公共方法

    詳解Java構(gòu)建樹結(jié)構(gòu)的公共方法

    本文主要介紹了詳解Java構(gòu)建樹結(jié)構(gòu)的公共方法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2023-04-04

最新評論

曲靖市| 新龙县| 辉南县| 红桥区| 剑阁县| 定结县| 永福县| 饶平县| 册亨县| 库车县| 安图县| 大足县| 紫金县| 托克托县| 绵竹市| 桂东县| 上饶县| 通山县| 中牟县| 盐山县| 凤山县| 安岳县| 繁昌县| 公主岭市| 临潭县| 曲靖市| 孟连| 桐乡市| 印江| 阳城县| 疏勒县| 新巴尔虎左旗| 雅安市| 都安| 双流县| 南木林县| 咸丰县| 安塞县| 崇礼县| 婺源县| 洪江市|