国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁 > 開發 > Java > 正文

自己動手在Spring-Boot上加強國際化功能的示例

2024-07-14 08:40:14
字體:
來源:轉載
供稿:網友

前言

公司將項目由Struts2轉到Springmvc了,由于公司業務是境外服務,所以對國際化功能需求很高。Struts2自帶的國際化功能相對Springmvc來說更加完善,不過spring很大的特性就是可定定制化性強,所以在公司項目移植的到Springmvc的時候增加了其國際化的功能。特此整理記錄并且完善了一下。

本文主要實現的功能:

從文件夾中直接加載多個國際化文件后臺設置前端頁面顯示國際化信息的文件利用攔截器和注解自動設置前端頁面顯示國際化信息的文件

注:本文不詳細介紹怎么配置國際化,區域解析器等。

實現

國際化項目初始化

先創建一個基本的Spring-Boot+thymeleaf+國際化信息(message.properties)項目,如果有需要可以從我的Github下載。

簡單看一下項目的目錄和文件

SpringBoot,國際化,Spring,Boot國際化功能

其中I18nApplication.java設置了一個CookieLocaleResolver,采用cookie來控制國際化的語言。還設置一個LocaleChangeInterceptor攔截器來攔截國際化語言的變化。

@SpringBootApplication@Configurationpublic class I18nApplication {  public static void main(String[] args) {    SpringApplication.run(I18nApplication.class, args);  }  @Bean  public LocaleResolver localeResolver() {    CookieLocaleResolver slr = new CookieLocaleResolver();    slr.setCookieMaxAge(3600);    slr.setCookieName("Language");//設置存儲的Cookie的name為Language    return slr;  }  @Bean  public WebMvcConfigurer webMvcConfigurer() {    return new WebMvcConfigurer() {      //攔截器      @Override      public void addInterceptors(InterceptorRegistry registry) {        registry.addInterceptor(new LocaleChangeInterceptor()).addPathPatterns("/**");      }    };  }}

我們再看一下hello.html中寫了什么:

<!DOCTYPE html><html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"><head>  <title>Hello World!</title></head><body><h1 th:text="#{i18n_page}"></h1><h3 th:text="#{hello}"></h3></body></html>

現在啟動項目并且訪問http://localhost:9090/hello(我在application.properties)中設置了端口為9090。

SpringBoot,國際化,Spring,Boot國際化功能

由于瀏覽器默認的語言是中文,所以他默認會去messages_zh_CN.properties中找,如果沒有就會去messages.properties中找國際化詞。

然后我們在瀏覽器中輸入http://localhost:9090/hello?locale=en_US,語言就會切到英文。同樣的如果url后參數設置為locale=zh_CH,語言就會切到中文。

SpringBoot,國際化,Spring,Boot國際化功能

從文件夾中直接加載多個國際化文件

在我們hello.html頁面中,只有'i18n_page'和'hello'兩個國際化信息,然而在實際項目中肯定不會只有幾個國際化信息那么少,通常都是成千上百個的,那我們肯定不能把這么多的國際化信息都放在messages.properties一個文件中,通常都是把國際化信息分類存放在幾個文件中。但是當項目大了以后,這些國際化文件也會越來越多,這時候在application.properties文件中一個個的去配置這個文件也是不方便的,所以現在我們實現一個功能自動加載制定目錄下所有的國際化文件。

繼承ResourceBundleMessageSource

在項目下創建一個類繼承ResourceBundleMessageSource或者ReloadableResourceBundleMessageSource,起名為MessageResourceExtension。并且注入到bean中起名為messageSource,這里我們繼承ResourceBundleMessageSource。

@Component("messageSource")public class MessageResourceExtension extends ResourceBundleMessageSource {}

注意這里我們的Component名字必須為'messageSource',因為在初始化ApplicationContext的時候,會查找bean名為'messageSource'的bean。這個過程在AbstractApplicationContext.java中,我們看一下源代碼

/*** Initialize the MessageSource.* Use parent's if none defined in this context.*/protected void initMessageSource() {  ConfigurableListableBeanFactory beanFactory = getBeanFactory();  if (beanFactory.containsLocalBean(MESSAGE_SOURCE_BEAN_NAME)) {    this.messageSource = beanFactory.getBean(MESSAGE_SOURCE_BEAN_NAME, MessageSource.class);  ...  }}...

在這個初始化MessageSource的方法中,beanFactory查找注入名為MESSAGE_SOURCE_BEAN_NAME(messageSource)的bean,如果沒有找到,就會在其父類中查找是否有該名的bean。

實現文件加載

現在我們可以開始在剛才創建的MessageResourceExtension

中寫加載文件的方法了。

@Component("messageSource")public class MessageResourceExtension extends ResourceBundleMessageSource {  private final static Logger logger = LoggerFactory.getLogger(MessageResourceExtension.class);  /**   * 指定的國際化文件目錄   */  @Value(value = "${spring.messages.baseFolder:i18n}")  private String baseFolder;  /**   * 父MessageSource指定的國際化文件   */  @Value(value = "${spring.messages.basename:message}")  private String basename;  @PostConstruct  public void init() {    logger.info("init MessageResourceExtension...");    if (!StringUtils.isEmpty(baseFolder)) {      try {        this.setBasenames(getAllBaseNames(baseFolder));      } catch (IOException e) {        logger.error(e.getMessage());      }    }    //設置父MessageSource        ResourceBundleMessageSource parent = new ResourceBundleMessageSource();    parent.setBasename(basename);    this.setParentMessageSource(parent);  }  /**   * 獲取文件夾下所有的國際化文件名   *   * @param folderName 文件名   * @return   * @throws IOException   */  private String[] getAllBaseNames(String folderName) throws IOException {    Resource resource = new ClassPathResource(folderName);    File file = resource.getFile();    List<String> baseNames = new ArrayList<>();    if (file.exists() && file.isDirectory()) {      this.getAllFile(baseNames, file, "");    } else {      logger.error("指定的baseFile不存在或者不是文件夾");    }    return baseNames.toArray(new String[baseNames.size()]);  }  /**   * 遍歷所有文件   *   * @param basenames   * @param folder   * @param path   */  private void getAllFile(List<String> basenames, File folder, String path) {    if (folder.isDirectory()) {      for (File file : folder.listFiles()) {        this.getAllFile(basenames, file, path + folder.getName() + File.separator);      }    } else {      String i18Name = this.getI18FileName(path + folder.getName());      if (!basenames.contains(i18Name)) {        basenames.add(i18Name);      }    }  }  /**   * 把普通文件名轉換成國際化文件名   *   * @param filename   * @return   */  private String getI18FileName(String filename) {    filename = filename.replace(".properties", "");    for (int i = 0; i < 2; i++) {      int index = filename.lastIndexOf("_");      if (index != -1) {        filename = filename.substring(0, index);      }    }    return filename;  }}

依次解釋一下幾個方法。

  1. init()方法上有一個@PostConstruct注解,這會在MessageResourceExtension類被實例化之后自動調用init()方法。這個方法獲取到baseFolder目錄下所有的國際化文件并設置到basenameSet中。并且設置一個ParentMessageSource,這會在找不到國際化信息的時候,調用父MessageSource來查找國際化信息。
  2. getAllBaseNames()方法獲取到baseFolder的路徑,然后調用getAllFile()方法獲取到該目錄下所有的國際化文件的文件名。
  3. getAllFile()遍歷目錄,如果是文件夾就繼續遍歷,如果是文件就調用getI18FileName()把文件名轉為'i18n/basename/‘格式的國際化資源名。

所以簡單來說就是在MessageResourceExtension被實例化之后,把'i18n'文件夾下的資源文件的名字,加載到Basenames中。現在來看一下效果。

首先我們在application.properties文件中添加一個spring.messages.baseFolder=i18n,這會把'i18n'這個值賦值給MessageResourceExtension中的baseFolder

在啟動后看到控制臺里打印出了init信息,表示被@PostConstruct注解的init()方法已經執行。

SpringBoot,國際化,Spring,Boot國際化功能

然后我們再創建兩組國際化信息文件:'dashboard'和'merchant',里面分別只有一個國際化信息:'dashboard.hello'和'merchant.hello'。

SpringBoot,國際化,Spring,Boot國際化功能

之后再修改一下hello.html文件,然后訪問hello頁面。

...<body><h1>國際化頁面!</h1><p th:text="#{hello}"></p><p th:text="#{merchant.hello}"></p><p th:text="#{dashboard.hello}"></p></body>...

SpringBoot,國際化,Spring,Boot國際化功能SpringBoot,國際化,Spring,Boot國際化功能

可以看到網頁中加載了'message','dashboard'和'merchant'中的國際化信息,說明我們已經成功一次性加載了'i18n'文件夾下的文件。

后臺設置前端頁面顯示國際化信息的文件

s剛才那一節我們成功加載了多個國際化文件并顯示出了他們的國際化信息。但是'dashboard.properties'中的國際化信息為'dashboard.hello'而'merchant.properties'中的是'merchant.hello',這樣每個都要寫一個前綴豈不是很麻煩,現在我想要在'dashboard'和'merchant'的國際化文件中都只寫'hello'但是顯示的是'dashboard'或'merchant'中的國際化信息。

MessageResourceExtension重寫resolveCodeWithoutArguments方法(如果有字符格式化的需求就重寫resolveCode方法)。

@Component("messageSource")public class MessageResourceExtension extends ResourceBundleMessageSource {  ...  public static String I18N_ATTRIBUTE = "i18n_attribute";    @Override  protected String resolveCodeWithoutArguments(String code, Locale locale) {    // 獲取request中設置的指定國際化文件名    ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();    final String i18File = (String) attr.getAttribute(I18N_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST);    if (!StringUtils.isEmpty(i18File)) {      //獲取在basenameSet中匹配的國際化文件名      String basename = getBasenameSet().stream()          .filter(name -> StringUtils.endsWithIgnoreCase(name, i18File))          .findFirst().orElse(null);      if (!StringUtils.isEmpty(basename)) {        //得到指定的國際化文件資源        ResourceBundle bundle = getResourceBundle(basename, locale);        if (bundle != null) {          return getStringOrNull(bundle, code);        }      }    }    //如果指定i18文件夾中沒有該國際化字段,返回null會在ParentMessageSource中查找    return null;  }  ...}

在我們重寫的resolveCodeWithoutArguments方法中,從HttpServletRequest中獲取到‘I18N_ATTRIBUTE'(等下再說這個在哪里設置),這個對應我們想要顯示的國際化文件名,然后我們在BasenameSet中查找該文件,再通過getResourceBundle獲取到資源,最后再getStringOrNull獲取到對應的國際化信息。

現在我們到我們的HelloController里加兩個方法。

@Controllerpublic class HelloController {  @GetMapping("/hello")  public String index(HttpServletRequest request) {    request.setAttribute(MessageResourceExtension.I18N_ATTRIBUTE, "hello");    return "system/hello";  }  @GetMapping("/dashboard")  public String dashboard(HttpServletRequest request) {    request.setAttribute(MessageResourceExtension.I18N_ATTRIBUTE, "dashboard");    return "dashboard";  }  @GetMapping("/merchant")  public String merchant(HttpServletRequest request) {    request.setAttribute(MessageResourceExtension.I18N_ATTRIBUTE, "merchant");    return "merchant";  }}

看到我們在每個方法中都設置一個對應的'I18N_ATTRIBUTE',這會在每次請求中設置對應的國際化文件,然后在MessageResourceExtension中獲取。

這時我們看一下我們的國際化文件,我們可以看到所有關鍵字都是'hello',但是信息卻不同。

SpringBoot,國際化,Spring,Boot國際化功能

SpringBoot,國際化,Spring,Boot國際化功能

SpringBoot,國際化,Spring,Boot國際化功能

同時新增兩個html文件分別是'dashboard.html'和'merchant.html',里面只有一個'hello'的國際化信息和用于區分的標題。

<!-- 這是hello.html --><body><h1>國際化頁面!</h1><p th:text="#{hello}"></p></body>
<!-- 這是dashboard.html --><body><h1>國際化頁面(dashboard)!</h1><p th:text="#{hello}"></p></body>
<!-- 這是merchant.html --><body><h1>國際化頁面(merchant)!</h1><p th:text="#{hello}"></p></body>

這時我們啟動項目看一下。

SpringBoot,國際化,Spring,Boot國際化功能

SpringBoot,國際化,Spring,Boot國際化功能

SpringBoot,國際化,Spring,Boot國際化功能

可以看到雖然在每個頁面的國際化詞都是'hello',但是我們在對應的頁面顯示了我們想要顯示的信息。

利用攔截器和注解自動設置前端頁面顯示國際化信息的文件

雖然已經可以指定對應的國際化信息,但是這樣要在每個controller里的HttpServletRequest中設置國際化文件實在太麻煩了,所以現在我們實現自動判定來顯示對應的文件。

首先我們創建一個注解,這個注解可以放在類上或者方法上。

@Target({ElementType.TYPE, ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)public @interface I18n {  /**   * 國際化文件名   */  String value();}

然后我們把這個創建的I18n 注解放在剛才的Controller方法中,為了顯示他的效果,我們再創建一個ShopControllerUserController,同時也創建對應的'shop'和'user'的國際化文件,內容也都是一個'hello'。

@Controllerpublic class HelloController {  @GetMapping("/hello")  public String index() {    return "system/hello";  }  @I18n("dashboard")  @GetMapping("/dashboard")  public String dashboard() {    return "dashboard";  }  @I18n("merchant")  @GetMapping("/merchant")  public String merchant() {    return "merchant";  }}
@I18n("shop")@Controllerpublic class ShopController {  @GetMapping("shop")  public String shop() {    return "shop";  }}
@Controllerpublic class UserController {  @GetMapping("user")  public String user() {    return "user";  }}

我們把I18n注解分別放在HelloController下的dashboardmerchant方法下,和ShopController類上。并且去除了原來dashboardmerchant方法下設置‘I18N_ATTRIBUTE'的語句。

準備工作都做好了,現在看看如何實現根據這些注解自動的指定國際化文件。

public class MessageResourceInterceptor implements HandlerInterceptor {  @Override  public void postHandle(HttpServletRequest req, HttpServletResponse rep, Object handler, ModelAndView modelAndView) {    // 在方法中設置i18路徑    if (null != req.getAttribute(MessageResourceExtension.I18N_ATTRIBUTE)) {      return;    }    HandlerMethod method = (HandlerMethod) handler;    // 在method上注解了i18    I18n i18nMethod = method.getMethodAnnotation(I18n.class);    if (null != i18nMethod) {      req.setAttribute(MessageResourceExtension.I18N_ATTRIBUTE, i18nMethod.value());      return;    }    // 在Controller上注解了i18    I18n i18nController = method.getBeanType().getAnnotation(I18n.class);    if (null != i18nController) {      req.setAttribute(MessageResourceExtension.I18N_ATTRIBUTE, i18nController.value());      return;    }    // 根據Controller名字設置i18    String controller = method.getBeanType().getName();    int index = controller.lastIndexOf(".");    if (index != -1) {      controller = controller.substring(index + 1, controller.length());    }    index = controller.toUpperCase().indexOf("CONTROLLER");    if (index != -1) {      controller = controller.substring(0, index);    }    req.setAttribute(MessageResourceExtension.I18N_ATTRIBUTE, controller);  }  @Override  public boolean preHandle(HttpServletRequest req, HttpServletResponse rep, Object handler) {    // 在跳轉到該方法先清除request中的國際化信息    req.removeAttribute(MessageResourceExtension.I18N_ATTRIBUTE);    return true;  }}

簡單講解一下這個攔截器。

首先,如果request中已經有'I18N_ATTRIBUTE',說明在Controller的方法中指定設置了,就不再判斷。

然后判斷一下進入攔截器的方法上有沒有I18n的注解,如果有就設置'I18N_ATTRIBUTE'到request中并退出攔截器,如果沒有就繼續。

再判斷進入攔截的類上有沒有I18n的注解,如果有就設置'I18N_ATTRIBUTE'到request中并退出攔截器,如果沒有就繼續。

最后假如方法和類上都沒有I18n的注解,那我們可以根據Controller名自動設置指定的國際化文件,比如'UserController'那么就會去找'user'的國際化文件。

現在我們再運行一下看看效果,看到每個鏈接都顯示的他們對應的國際化信息里的內容。

SpringBoot,國際化,Spring,Boot國際化功能

SpringBoot,國際化,Spring,Boot國際化功能

SpringBoot,國際化,Spring,Boot國際化功能

SpringBoot,國際化,Spring,Boot國際化功能

最后

剛才完成了我們整個國際化增強的基本功能,最后我把全部代碼整理了一下,并且整合了bootstrap4來展示了一下功能的實現效果。

SpringBoot,國際化,Spring,Boot國際化功能

SpringBoot,國際化,Spring,Boot國際化功能

SpringBoot,國際化,Spring,Boot國際化功能

SpringBoot,國際化,Spring,Boot國際化功能

詳細的代碼可以看我Github上Spring-Boot-I18n-Pro的代碼

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持VeVb武林網。


注:相關教程知識閱讀請移步到JAVA教程頻道。
發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
主站蜘蛛池模板: 石台县| 疏勒县| 康定县| 永宁县| 天台县| 姜堰市| 桐乡市| 瑞昌市| 中超| 隆子县| 赤峰市| 京山县| 满洲里市| 浙江省| 定兴县| 孟连| 静乐县| 翁牛特旗| 阳高县| 米泉市| 应城市| 吉木萨尔县| 永城市| 梅河口市| 晋中市| 天门市| 瑞丽市| 桐庐县| 济宁市| 辽宁省| 镇远县| 托里县| 禄劝| 手游| 台湾省| 临沭县| 靖安县| 罗甸县| 信宜市| 怀来县| 民和|