spring之i18n

简介:

国际化信息也称为本地化信息,i18n是“国际化”的简称。在资讯领域,国际化(i18n)指让产品无需做大的改变就能够适应不同的语言和地区的需要。对程序来说,在不修改内部代码的情况下,能根据不同语言及地区显示相应的界面。

demo

step 1. 新增国际化资源问题

在这里插入图片描述
分别在三个文件中添加内容如下:
message.properties:表示默认的,里面可以没有值,但必须有这样的一个文件,可以参见源码:MessageSourceAutoConfiguration.ResourceBundleCondition#getMatchOutcomeForBasename
zh_CH

10001=你好,世界
10002=你好 JAVA

en_US

10001=hello word
10002=hello JAVA
step 2. 配置资源文件位置
spring.messages.basename=i18n.message
step 3. 配置解析器

SessionLocaleResolver为spring内置解析器之一,主要处理session会话级语言解析,其他还有CookieLocaleResolverFixedLocaleResolverAcceptHeaderLocaleResolver

@Configuration
public class LocaleConfig {
    @Bean
    public LocaleResolver localeResolver() {
        SessionLocaleResolver localeResolver = new SessionLocaleResolver();
        localeResolver.setDefaultLocale(Locale.CHINA);//默认语言
        return localeResolver;
    }
}
step 4 配置拦截器

有了解析器,还需要拦截器来对请求的语言参数进行获取,采用默认的LocaleChangeInterceptor作为拦截器来指定切换国际化语言的参数名。比如当请求的url中包含?lang=zh_CN表示读取国际化文件messages_zh_CN.properties。

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        LocaleInterceptor localeInterceptor = new LocaleInterceptor();
        registry.addInterceptor(localeInterceptor);
    }
}
@Slf4j
public class LocaleInterceptor extends LocaleChangeInterceptor {
    private static final String LOCALE = "X-Locale";
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String newLocale = request.getHeader(LOCALE);
        if (newLocale != null) {
            LocaleResolver localeResolver = RequestContextUtils.getLocaleResolver(request);
            if (localeResolver == null) {
                throw new IllegalStateException("No LocaleResolver found: not in a DispatcherServlet request?");
            }
            try {
                localeResolver.setLocale(request, response, parseLocaleValue(newLocale));
            } catch (IllegalArgumentException e) {
                if (isIgnoreInvalidLocale()) {
                    log.warn("Ignoring invalid locale value [{}]: ", newLocale, e);
                } else {
                    throw e;
                }
            }
        }
        return true;
    }
}
step 5 . i18n工具类
@Slf4j
@Component
public class LocaleMessageUtils {

   @Autowired
   private MessageSource messageSource;

   public String getMessage(String code, Object[] args, String defaultMessage) {
       try {
           Locale locale = LocaleContextHolder.getLocale();
           return messageSource.getMessage(code, args, defaultMessage, locale);
       } catch (Exception e) {
           log.error("get locale message failed, ErrorMsg:" + e.getMessage());
           return defaultMessage;
       }
   }
}
step 6. 测试

在这里插入图片描述

在这里插入图片描述

源码分析

MessageSourceAutoConfiguration

ResourceBundleCondition作为是否加载该配置类的条件

   //该方法主要根据配置资源路径是否能找到对应的资源,只要能找到资源时才能加载此配置类
	@Override
		public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
			String basename = context.getEnvironment().getProperty("spring.messages.basename", "messages");
			ConditionOutcome outcome = cache.get(basename);
			if (outcome == null) {
				outcome = getMatchOutcomeForBasename(context, basename);
				cache.put(basename, outcome);
			}
			return outcome;
		}

springboot提供了国际化信息自动配置类,配置类中注册了ResourceBundleMessageSource实现类。

@Bean
	public MessageSource messageSource(MessageSourceProperties properties) {
		ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
		//设置资源路径
		if (StringUtils.hasText(properties.getBasename())) {
			messageSource.setBasenames(StringUtils
					.commaDelimitedListToStringArray(StringUtils.trimAllWhitespace(properties.getBasename())));
		}
		//设置资源文件默认编码
		if (properties.getEncoding() != null) {
			messageSource.setDefaultEncoding(properties.getEncoding().name());
		}
		//在找不到当前系统对应的资源文件时,如果该属性为 true,则会默认查找当前系统对应的资源文件,否则就返回 null,返回 null 之后,最终又会调用到系统默认的 messages.properties 文件
		messageSource.setFallbackToSystemLocale(properties.isFallbackToSystemLocale());
		//设置缓存过期时间
		Duration cacheDuration = properties.getCacheDuration();
		if (cacheDuration != null) {
			messageSource.setCacheMillis(cacheDuration.toMillis());
		}
		//该参数控制的是,当输入参数为空时,是否还是使用MessageFormat.format函数对结果进行格式化,默认是 false;
		messageSource.setAlwaysUseMessageFormat(properties.isAlwaysUseMessageFormat());
		//解析不到资源时是否用code作为返回值
		messageSource.setUseCodeAsDefaultMessage(properties.isUseCodeAsDefaultMessage());
		return messageSource;
	}

RequestContextFilter

springmvc自动装配配置类,注册了一个RequestContextFilter过滤器,一次请求,LocaleContextHolder都会保存当前请求的本地化信息,一般用于设置默认本地化信息。

//处理过滤器
@Override
	protected void doFilterInternal(
			HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {

		ServletRequestAttributes attributes = new ServletRequestAttributes(request, response);
		initContextHolders(request, attributes);

		try {
			filterChain.doFilter(request, response);
		}
		finally {
		  //请求结束清空本次请求本地化信息
			resetContextHolders();
			if (logger.isTraceEnabled()) {
				logger.trace("Cleared thread-bound request context: " + request);
			}
			attributes.requestCompleted();
		}
	}
	
	//初始化本次请求的本地化配置
	private void initContextHolders(HttpServletRequest request, ServletRequestAttributes requestAttributes) {
		LocaleContextHolder.setLocale(request.getLocale(), this.threadContextInheritable);
		RequestContextHolder.setRequestAttributes(requestAttributes, this.threadContextInheritable);
		if (logger.isTraceEnabled()) {
			logger.trace("Bound request context to thread: " + request);
		}
	}
   //重置清空吧本地化配置
	private void resetContextHolders() {
		LocaleContextHolder.resetLocaleContext();
		RequestContextHolder.resetRequestAttributes();
	}

buildLocaleContext

DispatcherServlet#LocaleContextHolder

在每次请求都会用注册的LocaleResolver构造LocaleContext实例,如SessionLocaleResolver#resolveLocaleContext。如果不是LocaleContextResolver的话就直接取HttpServletRequest中的Locale返回,然后this.initContextHolders方法将解析后的Locale对象设值到LocaleContextHolder中:

@Override
	protected LocaleContext buildLocaleContext(final HttpServletRequest request) {
		LocaleResolver lr = this.localeResolver;
		if (lr instanceof LocaleContextResolver) {
			return ((LocaleContextResolver) lr).resolveLocaleContext(request);
		}
		else {
			return () -> (lr != null ? lr.resolveLocale(request) : request.getLocale());
		}
	}

LocaleContextHolder是用来处理Local的上下文容器(其实就是内部维护了一个ThreadLocal),其中LocaleContext是一个用来获取Locale的接口

//设置当前请求上下文本地化信息
public static void setLocaleContext(@Nullable LocaleContext localeContext, boolean inheritable) {
		if (localeContext == null) {
			resetLocaleContext();
		}
		else {
			if (inheritable) {
				inheritableLocaleContextHolder.set(localeContext);
				localeContextHolder.remove();
			}
			else {
				localeContextHolder.set(localeContext);
				inheritableLocaleContextHolder.remove();
			}
		}
	}

ResourceBundleMessageSource

首先遍历各个basename, 如果资源文件没加载首先会根据basename、locale去加载资源,并将加载的内容缓存起来,然后从加载的资源文件中根据code查找对应的本地化message。该实接口实现类还有一个与之相近的版本ReloadableResourceBundleMessageSource,支持内容的过期个刷新。

@Override
	protected String resolveCodeWithoutArguments(String code, Locale locale) {
		Set<String> basenames = getBasenameSet();
		for (String basename : basenames) {
			ResourceBundle bundle = getResourceBundle(basename, locale);
			if (bundle != null) {
				String result = getStringOrNull(bundle, code);
				if (result != null) {
					return result;
				}
			}
		}
		return null;
	}

MessageSourceControl

负责资源的具体加载逻辑

@Nullable
		public ResourceBundle newBundle(String baseName, Locale locale, String format, ClassLoader loader, boolean reload)
				throws IllegalAccessException, InstantiationException, IOException {
			//资源格式:properties
			if (format.equals("java.properties")) {
			   //拼接资源名
				String bundleName = toBundleName(baseName, locale);
				final String resourceName = toResourceName(bundleName, "properties");
				final ClassLoader classLoader = loader;
				final boolean reloadFlag = reload;
				InputStream inputStream;
				try {
				    //加载资源文件流
					inputStream = AccessController.doPrivileged((PrivilegedExceptionAction<InputStream>) () -> {
						InputStream is = null;
						if (reloadFlag) {
							URL url = classLoader.getResource(resourceName);
							if (url != null) {
								URLConnection connection = url.openConnection();
								if (connection != null) {
									connection.setUseCaches(false);
									is = connection.getInputStream();
								}
							}
						}
						else {
							is = classLoader.getResourceAsStream(resourceName);
						}
						return is;
					});
				}
				catch (PrivilegedActionException ex) {
					throw (IOException) ex.getException();
				}
				if (inputStream != null) {
					String encoding = getDefaultEncoding();
					//设置文件编码
					if (encoding != null) {
						try (InputStreamReader bundleReader = new InputStreamReader(inputStream, encoding)) {
						  //用Properties在家文件流,在资源转换成key-value形式
							return loadBundle(bundleReader);
						}
					}
					else {
						try (InputStream bundleStream = inputStream) {
						  //用Properties在家文件流,在资源转换成key-value形式
							return loadBundle(bundleStream);
						}
					}
				}
				else {
					return null;
				}
			}
			else {
				//文件格式是class
				return super.newBundle(baseName, locale, format, loader, reload);
			}
		}
### 实现Spring Boot国际化配置 #### 添加依赖 为了使Spring Boot项目支持国际化,通常不需要额外添加Maven或Gradle依赖项,因为`spring-boot-starter-web`已经包含了必要的库。 #### 配置国际化资源文件 在项目的`resources`目录下创建多语言版本的消息属性文件。这些文件应遵循命名约定`messages_{language}.properties`[^2]。例如: - `messages_en.properties`: 英语版消息文件。 - `messages_zh_CN.properties`: 简体中文版消息文件。 每个`.properties`文件都包含键值对形式的信息条目,用于存储特定于该语言的文字表述。 #### 创建配置类并设置MessageSource Bean 可以编写一个Java配置类来定制化`MessageSource` bean的行为,这有助于更灵活地控制如何加载和解析消息源。下面是一个简单的例子: ```java @Configuration public class WebConfig implements WebMvcConfigurer { @Bean public MessageSource messageSource() { ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource(); // 设置basename为classpath下的路径加上基础名称 messageSource.setBasename("i18n/messages"); messageSource.setDefaultEncoding("UTF-8"); return messageSource; } } ``` 此代码片段展示了如何通过编程方式设定`MessageSource`的基础名以及字符编码格式[^4]。 #### 自定义LocaleResolver以改变默认的语言解析逻辑 如果希望应用程序能够根据用户的浏览器偏好或者其他条件动态切换显示语言,则可能需要注册自定义的`LocaleResolver`组件。比如基于URL参数或者HTTP头部信息决定当前会话所使用的区域设置。 ```java @Bean public LocaleResolver localeResolver(){ SessionLocaleResolver slr = new SessionLocaleResolver(); slr.setDefaultLocale(Locale.SIMPLIFIED_CHINESE); return slr; } ``` 上述实例中选择了会话级别的本地解决器(`SessionLocaleResolver`)并将简化汉字设为缺省选项[^5]。 #### 修改页面模板以便支持国际化 假设前端采用Thymeleaf作为视图渲染工具,在HTML文档里可以通过`${...}`语法访问由后端传递过来的数据对象;而对于静态文本部分则推荐利用`#{...}`表达式从已有的资源束中提取对应语言环境下的字符串描述。 例如: ```html <p th:text="#{welcome.message}"></p> <!-- 假定存在名为 welcome.message 的key --> ``` 这样做的好处是可以让开发者专注于业务逻辑开发的同时也方便后期维护者更新界面文案而无需改动任何程序代码。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值