摘要:還設置一個攔截器來攔截國際化語言的變化。修改啟動類攔截器現在我們再運行一下看看效果,看到每個鏈接都顯示的他們對應的國際化信息里的內容。
前言
公司將項目由Struts2轉到Springmvc了,由于公司業務是境外服務,所以對國際化功能需求很高。Struts2自帶的國際化功能相對Springmvc來說更加完善,不過spring很大的特性就是可定定制化性強,所以在公司項目移植的到Springmvc的時候增加了其國際化的功能。特此整理記錄并且完善了一下。
本文主要實現的功能:
從文件夾中直接加載多個國際化文件
后臺設置前端頁面顯示國際化信息的文件
利用攔截器和注解自動設置前端頁面顯示國際化信息的文件
注:本文不詳細介紹怎么配置國際化,區域解析器等。實現 國際化項目初始化
先創建一個基本的Spring-Boot+thymeleaf+國際化信息(message.properties)項目,如果有需要可以從我的Github下載。
簡單看一下項目的目錄和文件
其中I18nApplication.java設置了一個CookieLocaleResolver,采用cookie來控制國際化的語言。還設置一個LocaleChangeInterceptor攔截器來攔截國際化語言的變化。
@SpringBootApplication @Configuration public 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中寫了什么:
Hello World!
現在啟動項目并且訪問http://localhost:9090/hello(我在application.properties)中設置了端口為9090。
由于瀏覽器默認的語言是中文,所以他默認會去messages_zh_CN.properties中找,如果沒有就會去messages.properties中找國際化詞。
然后我們在瀏覽器中輸入http://localhost:9090/hello?locale=en_US,語言就會切到英文。同樣的如果url后參數設置為locale=zh_CH,語言就會切到中文。
從文件夾中直接加載多個國際化文件在我們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(); ListbaseNames = 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 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; } }
依次解釋一下幾個方法。
init()方法上有一個@PostConstruct注解,這會在MessageResourceExtension類被實例化之后自動調用init()方法。這個方法獲取到baseFolder目錄下所有的國際化文件并設置到basenameSet中。并且設置一個ParentMessageSource,這會在找不到國際化信息的時候,調用父MessageSource來查找國際化信息。
getAllBaseNames()方法獲取到baseFolder的路徑,然后調用getAllFile()方法獲取到該目錄下所有的國際化文件的文件名。
getAllFile()遍歷目錄,如果是文件夾就繼續遍歷,如果是文件就調用getI18FileName()把文件名轉為’i18n/basename/‘格式的國際化資源名。
所以簡單來說就是在MessageResourceExtension被實例化之后,把"i18n"文件夾下的資源文件的名字,加載到Basenames中。現在來看一下效果。
首先我們在application.properties文件中添加一個spring.messages.baseFolder=i18n,這會把"i18n"這個值賦值給MessageResourceExtension中的baseFolder。
在啟動后看到控制臺里打印出了init信息,表示被@PostConstruct注解的init()方法已經執行。
然后我們再創建兩組國際化信息文件:"dashboard"和"merchant",里面分別只有一個國際化信息:"dashboard.hello"和"merchant.hello"。
之后再修改一下hello.html文件,然后訪問hello頁面。
...國際化頁面!
...
可以看到網頁中加載了"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里加兩個方法。
@Controller public 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",但是信息卻不同。
同時新增兩個html文件分別是"dashboard.html"和"merchant.html",里面只有一個"hello"的國際化信息和用于區分的標題。
國際化頁面!
國際化頁面(dashboard)!
國際化頁面(merchant)!
這時我們啟動項目看一下。
可以看到雖然在每個頁面的國際化詞都是"hello",但是我們在對應的頁面顯示了我們想要顯示的信息。
利用攔截器和注解自動設置前端頁面顯示國際化信息的文件雖然已經可以指定對應的國際化信息,但是這樣要在每個controller里的HttpServletRequest中設置國際化文件實在太麻煩了,所以現在我們實現自動判定來顯示對應的文件。
首先我們創建一個注解,這個注解可以放在類上或者方法上。
@Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface I18n { /** * 國際化文件名 */ String value(); }
然后我們把這個創建的I18n 注解放在剛才的Controller方法中,為了顯示他的效果,我們再創建一個ShopController和UserController,同時也創建對應的"shop"和"user"的國際化文件,內容也都是一個"hello"。
@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類上。并且去除了原來dashboard和merchant方法下設置‘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"的國際化文件。
攔截器完成了,現在把攔截器配置到系統中。修改I18nApplication啟動類:
@SpringBootApplication @Configuration public class I18nApplication { ... @Bean public WebMvcConfigurer webMvcConfigurer() { return new WebMvcConfigurer() { //攔截器 @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LocaleChangeInterceptor()).addPathPatterns("/**"); registry.addInterceptor(new MessageResourceInterceptor()).addPathPatterns("/**"); } }; } }
現在我們再運行一下看看效果,看到每個鏈接都顯示的他們對應的國際化信息里的內容。
最后剛才完成了我們整個國際化增強的基本功能,最后我把全部代碼整理了一下,并且整合了bootstrap4來展示了一下功能的實現效果。
詳細的代碼可以看我Github上Spring-Boot-I18n-Pro的代碼
原文地址:http://zzzzbw.cn
2018/8/30更新文章發布后,有人向我提到當把項目打成jar包之后執行java -jar i18n-0.0.1.jar的方式來運行程序會報錯。看到這樣的反饋我立刻就意識到,確實在讀取i18n的國際化文件的時候用的是File的形式來讀取文件名的,假如打包成jar包后所有文件都是在壓縮文件夾中,就不能簡單的以File的形式來獲取到文件夾下的所有文件了。因為公司的項目是以war包的形式在Tomcat下運行,所以沒有發現這個問題。
主要問題是在MessageResourceExtension類在spring-boot啟動時讀取配置文件導致的,所以修改MessageResourceExtension:
@Component("messageSource") public class MessageResourceExtension extends ResourceBundleMessageSource { ... /** * 獲取文件夾下所有的國際化文件名 */ private String[] getAllBaseNames(final String folderName) throws IOException { URL url = Thread.currentThread().getContextClassLoader() .getResource(folderName); if (null == url) { throw new RuntimeException("無法獲取資源文件路徑"); } ListbaseNames = 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 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]); } /** * 把普通文件名轉換成國際化文件名 */ 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("", "/"); } ... }
在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的資源文件名了。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/69189.html
摘要:不過仔細了解了一段時候發現,其實他的原理是很簡單的,所以想要自己也動手實現一個功能類似的框架。原文地址從零開始實現一個簡易的框架 前言 最近在看spring-boot框架的源碼,看了源碼之后更是讓我感受到了spring-boot功能的強大。而且使用了很多的設計模式,讓人在看的時候覺得有點難以下手。 不過仔細了解了一段時候發現,其實他的原理是很簡單的,所以想要自己也動手實現一個功能類似的...
摘要:初衷看了一下相關的書籍,創建一個的應用,是那么的簡單。首先,我們只是創建一個簡單的并不打算使用默認的,而是使用傳統的。在下創建目錄并且在目錄下新建,內容為頁面。如果是在內置的的情況下,應用會自動重啟。 初衷 看了一下spring-boot相關的書籍,創建一個hello world!的應用,是那么的簡單。然而,自己動手,卻很不一樣。 首先,我們只是創建一個簡單的hello world!并...
摘要:服務器相關配置啟動類資源目錄目錄靜態文件目錄端口號目錄目錄實現內嵌服務器在上一章文章從零開始實現一個簡易的框架七實現已經在文件中引入了依賴,所以這里就不用引用了。 spring-boot的Starter 一個項目總是要有一個啟動的地方,當項目部署在tomcat中的時候,經常就會用tomcat的startup.sh(startup.bat)的啟動腳本來啟動web項目 而在spring-b...
摘要:來源標題把中關村率先建成具有全球影響力的科學城日前,北京市委書記蔡奇到海淀區就中關村科學城規劃建設調查研究。我們必須進一步提高站位,努力把中關村打造成科學家發明家創業者的天堂,率先建成具有全球影響力的科學城。 來源標題:把中關村率先建成具有全球影響力的科學城 日前,北京市委書記蔡奇到海淀區就中關村科學城規劃建設調查研究。他強調,建設全國科技創新中心是首都城市戰略定位也是北京創新發展的主要...
摘要:目前最新版本作為官方推薦模板引擎,而且支持純瀏覽器展現模板表達式在脫離運行環境下不污染結構是時候了解一番了。當前數目,從開始。 Thymeleaf 目前最新版本3.0Thymeleaf作為Spring-Boot官方推薦模板引擎,而且支持純HTML瀏覽器展現(模板表達式在脫離運行環境下不污染html結構).是時候了解一番了。 安裝與初始化配置 與Spring集成 org.thyme...
閱讀 3279·2021-10-11 11:08
閱讀 4423·2021-09-22 15:54
閱讀 911·2019-08-30 15:56
閱讀 864·2019-08-30 15:55
閱讀 3540·2019-08-30 15:52
閱讀 1352·2019-08-30 15:43
閱讀 1937·2019-08-30 11:14
閱讀 2503·2019-08-29 16:11