原作者

源码:https://github.com/zzzzbw/Spring-Boot-I18n-Pro
https://github.com/zzzzbw/Spring-Boot-I18n-Pro/tree/starter 【starter】分支
原文
https://zzzzbw.cn/article/7

这里只是一个整理,没有按步骤一步一步的复制,初学者请看原作者的原文

目的:

从文件夹中直接加载多个国际化文件
后台设置前端页面显示国际化信息的文件
利用拦截器和注解自动设置前端页面显示国际化信息的文件

因为业务需要,对此功能进行了扩展,新功能->
Springboot thymeleaf i18n国际化多语言选择->2.业务流程内部返回 对应的语言 https://blog.csdn.net/fenglailea/article/details/89882786

利用拦截器和注解自动设置前端页面显示国际化信息的文件

虽然已经可以指定对应的国际化信息,但是这样要在每个controller里的HttpServletRequest中设置国际化文件实在太麻烦了,所以现在我们实现自动判定来显示对应的文件。

首先我们创建一个注解,这个注解可以放在类上或者方法上。

package com.zbw.i18n.annotation;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;/*** @auther zbw* @create 2018/4/4 17:59*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface I18n {/*** 国际化文件名*/String value();
}

然后我们把这个创建的I18n注解放在DashboardController控制器中或控制器的方法中,控制器方法中看后续部分案例

package com.zbw.i18n.controller;import com.zbw.i18n.annotation.I18n;
import com.zbw.i18n.model.Merchant;
import com.zbw.i18n.model.Shop;
import com.zbw.i18n.model.User;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;import java.util.ArrayList;
import java.util.List;/*** @author zbw* @create 2018/4/3 16:23*/
@I18n("dashboard")
@Controller
@RequestMapping("dashboard")
public class DashboardController {@GetMappingpublic String dashboard(Model model) {List<Merchant> merchants = new ArrayList<>();for (int i = 0; i < 10; i++) {Merchant merchant = new Merchant();merchants.add(merchant);}List<Shop> shops = new ArrayList<>();for (int i = 0; i < 15; i++) {Shop shop = new Shop();shops.add(shop);}List<User> users = new ArrayList<>();for (int i = 0; i < 5; i++) {User user = new User();users.add(user);}model.addAttribute("merchants", merchants);model.addAttribute("shops", shops);model.addAttribute("users", users);return "system/dashboard";}
}

为了显示他的效果,我们再创建一个HelloController,同时也创建对应的’demo’的国际化文件,内容也都是一个’hello’。

注意这个HelloController 在源码中是没有的,在 starter 分支中才有

@Controller
public 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")
@Controller
public class ShopController {@GetMapping("shop")public String shop() {return "shop";}
}
@Controller
public class UserController {@GetMapping("user")public String user() {return "user";}
}

我们把I18n注解分别放在HelloController下的dashboard和merchant方法下,和ShopController类上。
准备工作都做好了,现在看看如何实现根据这些注解自动的指定国际化文件。

package com.zbw.i18n.compoment;import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;import javax.annotation.PostConstruct;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.ResourceBundle;
import java.util.jar.JarFile;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;/*** @author zbw* @create 2018/4/3 18:20*/
@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;public static String I18N_ATTRIBUTE = "i18n_attribute";@PostConstructpublic void init() {logger.info("init MessageResourceExtension...");if (!StringUtils.isEmpty(baseFolder)) {try {this.setBasenames(getAllBaseNames(baseFolder));} catch (IOException e) {logger.error(e.getMessage());}}//设置父MessageSourceResourceBundleMessageSource parent = new ResourceBundleMessageSource();//是否是多个目录if (basename.indexOf(",") > 0) {parent.setBasenames(basename.split(","));} else {parent.setBasename(basename);}//设置文件编码parent.setDefaultEncoding("UTF-8");this.setParentMessageSource(parent);}@Overrideprotected 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;}/*** 获取文件夹下所有的国际化文件名** @param folderName 文件名* @return* @throws IOException*/private String[] getAllBaseNames(final String folderName) throws IOException {URL url = Thread.currentThread().getContextClassLoader().getResource(folderName);if (null == url) {throw new RuntimeException("无法获取资源文件路径");}List<String> baseNames = new ArrayList<>();if (url.getProtocol().equalsIgnoreCase("file")) {// 文件夹形式,用File获取资源路径File file = new File(url.getFile());if (file.exists() && file.isDirectory()) {baseNames = Files.walk(file.toPath()).filter(path -> path.toFile().isFile()).map(Path::toString).map(path -> path.substring(path.indexOf(folderName))).map(this::getI18FileName).distinct().collect(Collectors.toList());} else {logger.error("指定的baseFile不存在或者不是文件夹");}} else if (url.getProtocol().equalsIgnoreCase("jar")) {// jar包形式,用JarEntry获取资源路径String jarPath = url.getFile().substring(url.getFile().indexOf(":") + 2, url.getFile().indexOf("!"));JarFile jarFile = new JarFile(new File(jarPath));List<String> baseJars = jarFile.stream().map(ZipEntry::toString).filter(jar -> jar.endsWith(folderName + "/")).collect(Collectors.toList());if (baseJars.isEmpty()) {logger.info("不存在{}资源文件夹", folderName);return new String[0];}baseNames = jarFile.stream().map(ZipEntry::toString).filter(jar -> baseJars.stream().anyMatch(jar::startsWith)).filter(jar -> jar.endsWith(".properties")).map(jar -> jar.substring(jar.indexOf(folderName))).map(this::getI18FileName).distinct().collect(Collectors.toList());}return baseNames.toArray(new String[0]);}/*** 把普通文件名转换成国际化文件名** @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.replace("\\", "/");}
}

简单讲解一下这个拦截器。

首先,如果request中已经有I18N_ATTRIBUTE,说明在Controller的方法中指定设置了,就不再判断。
然后判断一下进入拦截器的方法上有没有I18n的注解,如果有就设置I18N_ATTRIBUTErequest中并退出拦截器,如果没有就继续。
再判断进入拦截的类上有没有I18n的注解,如果有就设置I18N_ATTRIBUTErequest中并退出拦截器,如果没有就继续。
最后假如方法和类上都没有I18n的注解,那我们可以根据Controller名自动设置指定的国际化文件,比如UserController那么就会去找user的国际化文件。

MessageResourceExtension重写resolveCodeWithoutArguments方法(如果有字符格式化的需求就重写resolveCode方法)。
在我们重写的resolveCodeWithoutArguments方法中,从HttpServletRequest中获取到‘I18N_ATTRIBUTE’(等下再说这个在哪里设置),这个对应我们想要显示的国际化文件名,然后我们在BasenameSet中查找该文件,再通过getResourceBundle获取到资源,最后再getStringOrNull获取到对应的国际化信息

resolveCodeWithoutArguments为了解决多写前缀问题。 dashboard.properties中的国际化信息为dashboard.hellomerchant.properties中的是merchant.hello,这样每个都要写一个前缀岂不是很麻烦,现在我想要在dashboardmerchant的国际化文件中都只写’hello’但是显示的是dashboardmerchant的国际化信息。

拦截器完成了,现在把拦截器配置到系统中。修改I18nApplication启动类:

package com.zbw.i18n;import com.zbw.i18n.interceptor.MessageResourceInterceptor;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
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.CookieLocaleResolver;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;import java.util.Locale;@SpringBootApplication
@Configuration
public class I18nApplication {public static void main(String[] args) {SpringApplication.run(I18nApplication.class, args);}@Beanpublic LocaleResolver localeResolver() {CookieLocaleResolver slr = new CookieLocaleResolver();slr.setDefaultLocale(Locale.CHINA);slr.setCookieMaxAge(3600);slr.setCookieName("Language");return slr;}@Beanpublic WebMvcConfigurer webMvcConfigurer() {return new WebMvcConfigurer() {//拦截器@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new LocaleChangeInterceptor()).addPathPatterns("/**");registry.addInterceptor(new MessageResourceInterceptor()).addPathPatterns("/**");}};}
}

其中I18nApplication.java设置了一个CookieLocaleResolver,采用cookie来控制国际化的语言。设置LocaleChangeInterceptorMessageResourceInterceptor拦截器来拦截国际化语言的变化。

继承ResourceBundleMessageSource

package com.zbw.i18n.compoment;import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;import javax.annotation.PostConstruct;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.ResourceBundle;
import java.util.jar.JarFile;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;/*** @author zbw* @create 2018/4/3 18:20*/
@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;public static String I18N_ATTRIBUTE = "i18n_attribute";@PostConstructpublic void init() {logger.info("init MessageResourceExtension...");if (!StringUtils.isEmpty(baseFolder)) {try {this.setBasenames(getAllBaseNames(baseFolder));} catch (IOException e) {logger.error(e.getMessage());}}//设置父MessageSourceResourceBundleMessageSource parent = new ResourceBundleMessageSource();//是否是多个目录if (basename.indexOf(",") > 0) {parent.setBasenames(basename.split(","));} else {parent.setBasename(basename);}//设置文件编码parent.setDefaultEncoding("UTF-8");this.setParentMessageSource(parent);}@Overrideprotected 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;}/*** 获取文件夹下所有的国际化文件名** @param folderName 文件名* @return* @throws IOException*/private String[] getAllBaseNames(final String folderName) throws IOException {URL url = Thread.currentThread().getContextClassLoader().getResource(folderName);if (null == url) {throw new RuntimeException("无法获取资源文件路径");}List<String> baseNames = new ArrayList<>();if (url.getProtocol().equalsIgnoreCase("file")) {// 文件夹形式,用File获取资源路径File file = new File(url.getFile());if (file.exists() && file.isDirectory()) {baseNames = Files.walk(file.toPath()).filter(path -> path.toFile().isFile()).map(Path::toString).map(path -> path.substring(path.indexOf(folderName))).map(this::getI18FileName).distinct().collect(Collectors.toList());} else {logger.error("指定的baseFile不存在或者不是文件夹");}} else if (url.getProtocol().equalsIgnoreCase("jar")) {// jar包形式,用JarEntry获取资源路径String jarPath = url.getFile().substring(url.getFile().indexOf(":") + 2, url.getFile().indexOf("!"));JarFile jarFile = new JarFile(new File(jarPath));List<String> baseJars = jarFile.stream().map(ZipEntry::toString).filter(jar -> jar.endsWith(folderName + "/")).collect(Collectors.toList());if (baseJars.isEmpty()) {logger.info("不存在{}资源文件夹", folderName);return new String[0];}baseNames = jarFile.stream().map(ZipEntry::toString).filter(jar -> baseJars.stream().anyMatch(jar::startsWith)).filter(jar -> jar.endsWith(".properties")).map(jar -> jar.substring(jar.indexOf(folderName))).map(this::getI18FileName).distinct().collect(Collectors.toList());}return baseNames.toArray(new String[0]);}/*** 把普通文件名转换成国际化文件名** @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.replace("\\", "/");}
}

在项目下创建一个类继承ResourceBundleMessageSource或者ReloadableResourceBundleMessageSource,起名为MessageResourceExtension。并且注入到bean中起名为messageSource,这里我们继承ResourceBundleMessageSource
注意这里我们的Component名字必须为messageSource,因为在初始化ApplicationContext的时候,会查找bean名为messageSourcebean。这个过程在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 依次解释一下几个方法。

1.init()方法上有一个@PostConstruct注解,这会在MessageResourceExtension类被实例化之后自动调用init()方法。这个方法获取到baseFolder目录下所有的国际化文件并设置到basenameSet中。并且设置一个ParentMessageSource,这会在找不到国际化信息的时候,调用父MessageSource来查找国际化信息。
2. getAllBaseNames()方法获取到baseFolder的路径,然后调用getAllFile()方法获取到该目录下所有的国际化文件的文件名。
3. getAllFile()遍历目录,如果是文件夹就继续遍历,如果是文件就调用getI18FileName()把文件名转为i18n/basename/格式的国际化资源名。

getAllBaseNames()方法中会先判断项目的Url形式为文件形式还是jar包形式。
如果是文件形式则就以普通文件夹的方式读取,这里还用了java8中的Files.walk()方法获取到文件夹下的所有文件,比原来自己写递归来读取方便多了。
如果是jar包的形式,那么就要用JarEntry来处理文件了。
首先是获取到项目jar包所在的的目录,如E:/workspace/java/Spring-Boot-I18n-Pro/target/i18n-0.0.1.jar这种,然后根据该目录new一个JarFile
接着遍历这个JarFile包下的资源,这会把我们项目jar包下的所有文件都读取出来,所以我们要先找到我们i18n资源文件所在的目录,通过.filter(jar -> jar.endsWith(folderName + "/"))获取资源所在目录。
接下来就是判断JarFile包下的文件是否在i18n资源目录了,如果是则调用getI18FileName()方法将其格式化成我们所需要的名字形式。
经过这段操作就实现了获取jar包下i18n的资源文件名了。

所以简单来说就是在MessageResourceExtension被实例化之后,把’i18n’文件夹下的资源文件的名字,加载到Basenames中。现在来看一下效果。

首先我们在application.properties文件中添加一个spring.messages.baseFolder=i18n,这会把i18n这个值赋值给MessageResourceExtension中的baseFolder

在启动后看到控制台里打印出了init信息,表示被@PostConstruct注解的init()方法已经执行。

 [           main] c.z.i.c.MessageResourceExtension         : init MessageResourceExtension...

然后我们再创建两组国际化信息文件:dashboardmerchant,里面分别只有一个国际化信息:dashboard.hellomerchant.hello

源码:https://github.com/zzzzbw/Spring-Boot-I18n-Pro
https://zzzzbw.cn/article/7

Springboot thymeleaf i18n国际化多语言选择相关推荐

  1. SpringBoot系列之i18n国际化多语言支持教程

    SpringBoot系列之i18n国际化多语言支持教程 文章目录 1.环境搭建 2.resource bundle资源配置 3.LocaleResolver类 4.I18n配置类 5.Thymelea ...

  2. springboot配置i18n国际化

    springboot实现i18n国际化,无需引入其他jar包,springboot已经内置了,只需要配置即可. 国际化主要是根据不同的国际语言来决定返回数据的语言. 添加properties文件,作为 ...

  3. 浏览器扩展开发 - i18n 国际化多语言配置

    原文地址:https://waynegong.cn/posts/954.html Chrome 浏览器扩展进行 i18n 国际化多语言适配需要进行三处修改: 在特定目录按照特定格式编写多语言配置的 m ...

  4. 【vue-element-admin】4.x 添加 i18n 国际化多语言切换

    花裤衩前辈的vue-element-admin模块在4.x的大版本中去除了对i18n国际化的支持,本次因项目需要,在一个基于 vue-element-admin V4.2.1 版本模板开发的项目中,需 ...

  5. ☘gMIS吉密斯i18n国际化多语言更新

    gMIS吉密斯部署和使用范围日益扩大,跨国多语言版本成为迫切需要的功能,早在2018年年中就考虑要增加这一功能--在gMIS吉密斯中实现多语言版本的支持.以期实现gMIS吉密斯的跨行业.跨地区和国际化 ...

  6. SpringBoot 系列教程(四十六):SpringBoot集成i18n国际化配置

    一.概述 软件的国际化:软件开发时,要使它能同时应对世界不同地区和国家的访问,并针对不同地区和国家的访问,提供相应的.符合来访者阅读习惯的页面或数据. 国际化(internationalization ...

  7. SpringBoot实现i18n国际化配置(超详细之跟着走就会系列)

    一.新增国际化资源文件 在resources文件下新建i18n文件,并新建国际化资源文件.如图: 点击新增Resource Bundle文件. 我们在Resource bundle base name ...

  8. angular i18n 国际化 多语言

    参考扬帆天下博客:http://www.cnblogs.com/yangfantianxia/p/7878823.html 在他的基础上把设置语言的部分写在app.component.ts里,这样就变 ...

  9. SpringBoot RESTful 风格 API 多语言国际化i18n解决方案

    文章目录 1 摘要 2 核心代码 2.1 多语言枚举类 2.2 多语言处理工具类 2.3 多语言的API返回状态码枚举类 2.4 多语言 API 接口返回结果封装 2.5 i18n 国际化多语言配置文 ...

  10. 一篇文章解决springboot+thymeleaf多语言国际化

    1.前言 博主最近在写一个多语言的项目,因为之前没实际接触过多语言的设计,所以写这篇文章记录下这次多语言开发的过程. 博主的开发环境是:Springboot1.5.6 + thymeleaf,需要注意 ...

最新文章

  1. append 降低数组位数_腿粗有理!研究发现腿部脂肪多,能大幅降低患高血压的风险!...
  2. Django使用缓存笔记
  3. 已解决AttributeError set object has no attribute items(亲测)
  4. css跑道_如何不超出跑道:计划种子的简单方法
  5. 查询ElasticSearch:用SQL代替DSL
  6. Python系列之入门篇——HDFS
  7. 全网独家分享,软件测试就该这么学,3个月进大厂!
  8. macbook数据线连接手机_MacBook可以为iPhone进行快充吗?用MacBook为iPhone充电好不好?...
  9. java家谱树_青锋家谱系统-基于springboot+orgtree的青锋家谱树管理系统
  10. SPSS统计描述分析
  11. python 将繁体转换成简体
  12. 计算机如何启动论文,论文在电脑上开始怎么写_初学者怎么在电脑上写论文_在电脑上写稿子的全部步骤...
  13. 营养保健品公司网站建设策划书
  14. Linux下tftp服务器/客户端安装
  15. PHP中利用PHPMailer配合QQ邮箱实现发邮件
  16. Pytorch:NLP 迁移学习、NLP中的标准数据集、NLP中的常用预训练模型、加载和使用预训练模型、huggingface的transfomers微调脚本文件
  17. 使用单选框、复选框,让用户选择
  18. UVa514 Rails(铁轨)
  19. 零数科技创始人林乐博士荣登“长三角G60科创走廊创业榜单—星耀G60”
  20. Certificate doesn't match any of the subject alternative names问题的解决

热门文章

  1. UVA 10246 Asterix and Obelix
  2. WIN32API之常用进程、线程函数
  3. 索罗斯说,我投机了,但我不觉得我做错了什么,我做的都是合法的。
  4. BOW( opencv源码)
  5. 拓端tecdat|R语言计算资本资产定价模型(CAPM)中的Beta值和可视化
  6. python定义一维数组
  7. java netty rpc框架_Java编写基于netty的RPC框架
  8. 清华大学操作系统OS学习(六)——进程和线程
  9. 计算机地图制图的点状符号制作,计算机地图制图验手册汇编.doc
  10. ps2018 html面板,28组经典PS 2018插件合集