目 录CONTENT

文章目录

SpringBoot 国际化配置

Jinty
2024-04-28 / 0 评论 / 0 点赞 / 33 阅读 / 16874 字

前言

LocaleResolver接口

基于 Web 的区域设置解析策略的接口,允许通过请求进行区域设置解析,以及通过请求和响应进行区域设置修改。

此接口允许基于请求、会话、cookie 等的实现。默认实现是 org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver,只需使用相应 HTTP 标头提供的请求区域设置即可。

用于 org.springframework.web.servlet.support.RequestContext.getLocale() 检索控制器或视图中的当前区域设置,与实际解析策略无关。

注意:从Spring 4.0开始,有一个名为 LocaleContextResolver的扩展策略接口,允许解析 org.springframework.context.i18n.LocaleContext 对象,可能包括相关的时区信息。Spring 提供的解析器实现在适当的时候实现了扩展 LocaleContextResolver 接口。

AcceptHeaderLocaleResolver

LocaleResolver 实现,它仅使用 HTTP 请求的 Accept-Language 标头中指定的主区域设置(即客户端浏览器发送的区域设置,通常是客户端操作系统的区域设置)。

MessageSource接口

用于解析消息的策略接口,支持此类消息的参数化和国际化。

Spring 为生产环境提供了两种开箱即用的实现:

  • org.springframework.context.support.ResourceBundleMessageSource:建立在标准 java.util.ResourceBundle之上,分享其局限性。

  • org.springframework.context.support.ReloadableResourceBundleMessageSource:高度可配置,特别是在重新加载消息定义方面。

MessageSourceAutoConfiguration

源码:

/*
 * Copyright 2012-2023 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.boot.autoconfigure.context;

import java.time.Duration;

import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigureOrder;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionMessage;
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.SearchStrategy;
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
import org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration.ResourceBundleCondition;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.core.Ordered;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.util.ConcurrentReferenceHashMap;
import org.springframework.util.StringUtils;

/**
 * {@link EnableAutoConfiguration Auto-configuration} for {@link MessageSource}.
 *
 * @author Dave Syer
 * @author Phillip Webb
 * @author Eddú Meléndez
 * @since 1.5.0
 */
@AutoConfiguration
@ConditionalOnMissingBean(name = AbstractApplicationContext.MESSAGE_SOURCE_BEAN_NAME, search = SearchStrategy.CURRENT)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@Conditional(ResourceBundleCondition.class)
@EnableConfigurationProperties
public class MessageSourceAutoConfiguration {

	private static final Resource[] NO_RESOURCES = {};

	@Bean
	@ConfigurationProperties(prefix = "spring.messages")
	public MessageSourceProperties messageSourceProperties() {
		return new MessageSourceProperties();
	}

	@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());
		}
		messageSource.setFallbackToSystemLocale(properties.isFallbackToSystemLocale());
		Duration cacheDuration = properties.getCacheDuration();
		if (cacheDuration != null) {
			messageSource.setCacheMillis(cacheDuration.toMillis());
		}
		messageSource.setAlwaysUseMessageFormat(properties.isAlwaysUseMessageFormat());
		messageSource.setUseCodeAsDefaultMessage(properties.isUseCodeAsDefaultMessage());
		return messageSource;
	}

	protected static class ResourceBundleCondition extends SpringBootCondition {

		private static ConcurrentReferenceHashMap<String, ConditionOutcome> cache = new ConcurrentReferenceHashMap<>();

		@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;
		}

		private ConditionOutcome getMatchOutcomeForBasename(ConditionContext context, String basename) {
			ConditionMessage.Builder message = ConditionMessage.forCondition("ResourceBundle");
			for (String name : StringUtils.commaDelimitedListToStringArray(StringUtils.trimAllWhitespace(basename))) {
				for (Resource resource : getResources(context.getClassLoader(), name)) {
					if (resource.exists()) {
						return ConditionOutcome.match(message.found("bundle").items(resource));
					}
				}
			}
			return ConditionOutcome.noMatch(message.didNotFind("bundle with basename " + basename).atAll());
		}

		private Resource[] getResources(ClassLoader classLoader, String name) {
			String target = name.replace('.', '/');
			try {
				return new PathMatchingResourcePatternResolver(classLoader)
					.getResources("classpath*:" + target + ".properties");
			}
			catch (Exception ex) {
				return NO_RESOURCES;
			}
		}

	}

}

通过阅读MessageSource自动配置代码可知:

  • 使用ResourceBundleMessageSource作为消息源

  • 配置文件应位于classpath路径下,文件后缀.properties,为通过spring.messages.basename 指定,可配置多个,可通过.或者/配置多级路径

应用实践一、基本使用

新建资源包

在resources下新建i18n路径,在i18n下新建资源包:

资源包基名称:messages

添加区域设置:如en、zh_CN等

messages_en.properties:

demo=demo-en

messages_zh_CN.properties:

demo={0},中文,{1}

配置

spring:
  messages:
    beannames: i18n/messages

测试接口

@RestController
@RequestMapping("/i18n")
public class TestApi
        extends AcceptHeaderLocaleResolver {
    @Autowired
    private MessageSource messageSource;

    @GetMapping("/get")
    public String get(@RequestHeader(name = "Accept-Language") String lang, HttpServletRequest request) {
        System.out.println(lang);
        return messageSource.getMessage("demo", new Object[]{"你好", "I18n"}, request.getLocale());
    }
}

测试结果:

注意

  • 请求的参数不是下划线_而是-

应用实践二、自定义配置

自定义配置属性


import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.support.AbstractResourceBasedMessageSource;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;

import java.util.Locale;

/**
 * 国际化配置类
 */
@Data
@ConfigurationProperties(prefix = "jcext.i18n")
public class I18nProperties {
    private boolean enabled = true;

    /**
     * 系统默认的应用场所
     */
    private Locale defaultLocale = Locale.CHINA;

    /**
     * 配置加载国际化资源文件的类默认使用 ReloadableResourceBundleMessageSource
     * ResourceBundleMessageSource是Spring框架提供的用于加载国际化资源文件的类,它是基于Java标准的ResourceBundle实现的。
     * 它在应用启动时会加载所有的资源文件,并且不支持动态刷新资源文件。
     * <p>
     * ReloadableResourceBundleMessageSource是对ResourceBundleMessageSource的扩展,
     * 它支持动态刷新资源文件。当资源文件发生变化时,可以通过调用特定的方法来重新加载资源文件,而不需要重启应用程序。
     */
    private Class<? extends AbstractResourceBasedMessageSource> messageSourceClass = ReloadableResourceBundleMessageSource.class;

    /**
     * 设置国际化文件存储路径和名称 i18n目录,messages文件名,需要编译打包后的资源文件的位置
     */
//    private String[] beannames = new String[]{"i18n/message"};
    private String[] beannames = new String[]{"classpath:i18n/message"};

    /**
     * 根据code如果没有获取到对应的文本信息,则返回code作为信息
     */
    private boolean useCode = false;

    /**
     * 设置字符编码
     */
    private String defaultEncoding = "UTF-8";
}

自定义解析

package cn.com.jcoo.ext.i18n;

import lombok.RequiredArgsConstructor;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.LocaleResolver;

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

/**
 * 配置http请求Locale解析器
 */
@RequiredArgsConstructor
public class HttpRequestLocaleResolver implements LocaleResolver {
    private final HttpServletRequest httpServletRequest;

    public Locale getRequestLocale() {
        return resolveLocale(httpServletRequest);
    }

    @Override
    public Locale resolveLocale(HttpServletRequest request) {
//		String lang = request.getHeader("lang");
        String lang = request.getParameter("lang");
        if (!StringUtils.hasLength(lang)) {
            return Locale.getDefault();
        }
        // return new Locale(lang);
        if (lang.contains("-")) {
            String[] split = lang.split("_");
            return new Locale(split[0], split[1]);
        }
        return new Locale(lang);

    }

    /**
     * 用于实现Locale的切换。比如SessionLocaleResolver获取Locale的方式是从session中读取,但如果
     * 用户想要切换其展示的样式(由英文切换为中文),那么这里的setLocale()方法就提供了这样一种可能
     *
     * @param request  请求
     * @param response 响应
     * @param locale   本地化配置
     */
    @Override
    public void setLocale(HttpServletRequest request, HttpServletResponse response,
                          Locale locale) {

    }
}

自定义工具组件

package cn.com.jcoo.ext.i18n;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.util.StringUtils;

import java.util.Locale;

@Slf4j
@RequiredArgsConstructor
public class I18nHelper {


    private static MessageSource messageSource;
    private static HttpRequestLocaleResolver httpRequestLocaleResolver;

    @Autowired
    public void setMessageSource(MessageSource messageSource) {
        I18nHelper.messageSource = messageSource;
    }

    @Autowired
    public void setHttpRequestLocaleResolver(HttpRequestLocaleResolver resolver) {
        I18nHelper.httpRequestLocaleResolver = resolver;
    }

    public static String getMessage(String code) {
        Locale locale = httpRequestLocaleResolver.getRequestLocale();
        return getMessage(code, null, "", locale);
    }

    public static String getMessage(String code, String lang) {
        Locale locale;
        if (!StringUtils.hasLength(lang)) {
            locale = Locale.getDefault();
        } else {
            try {
                locale = new Locale(lang);
            } catch (Exception e) {
                locale = Locale.getDefault();
            }
        }
        return getMessage(code, null, code, locale);
    }

    public static String getMessage(String code, Object[] args, String defaultMessage,
                                    Locale locale) {
        String content;
        try {
            content = messageSource.getMessage(code, args, locale);
        } catch (Exception e) {
            log.error("国际化参数获取失败===>{}", e.getMessage(), e);
            content = defaultMessage;
        }
        return content;

    }
}

自定义自动配置

package cn.com.jcoo.ext.i18n;

import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.AbstractResourceBasedMessageSource;

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


@Slf4j
@Configuration
@EnableConfigurationProperties(I18nProperties.class)
@ConditionalOnProperty(prefix = "jcext.i18n", name = "enabled", havingValue = "true", matchIfMissing = true)
public class I18nConfig {
    public static final String JCEXT_I18N_MS_BEAN_NAME = "JcextI18nMessageSource";

    private final I18nProperties i18nProperties;


    public I18nConfig(I18nProperties i18nProperties) {
        this.i18nProperties = i18nProperties;
    }

    @SneakyThrows
    @Bean(JCEXT_I18N_MS_BEAN_NAME)
    public MessageSource messageSource() {
        Locale.setDefault(i18nProperties.getDefaultLocale());
        Class<? extends AbstractResourceBasedMessageSource> messageSourceClass = i18nProperties.getMessageSourceClass();
        AbstractResourceBasedMessageSource source = messageSourceClass.newInstance();
        source.setBasenames(i18nProperties.getBeannames());
        source.setUseCodeAsDefaultMessage(false);
        source.setDefaultEncoding(i18nProperties.getDefaultEncoding());
        log.info("消息资源绑定 {}", Arrays.toString(i18nProperties.getBeannames()));
        return source;
    }

    @Bean
    @ConditionalOnBean(MessageSource.class)
    public HttpRequestLocaleResolver httpRequestLocaleResolver(HttpServletRequest httpServletRequest) {
        return new HttpRequestLocaleResolver(httpServletRequest);
    }

    @Bean
    @ConditionalOnBean(value = {MessageSource.class, HttpRequestLocaleResolver.class})
    public I18nHelper i18nHelper() {
        return new I18nHelper();
    }
}

简单使用

@Slf4j
@RestController
@RequestMapping("/test")
@RequiredArgsConstructor
public class TestApi {

	@GetMapping("/i18n/hello")
	public String i18nHello() {
		return I18nHelper.getMessage("hello");
	}
}

0

评论区