Nacos enhances the internationalization of SpringBoot

Keywords: Spring Java Session Lombok

1, Overview

Before reading this article, you should understand the internationalization implementation and principle of SpringBoot. Here is a brief introduction:

1. internationalization

Internationalization, also known as i18n (named because the word has 18 English letters from i to n). For some application systems, it needs to be published to different countries and regions, so it needs special methods to support, that is, internationalization. Through internationalization, the interface information, various prompt information and other contents can be flexibly displayed according to different countries and regions. For example, in China, the system is displayed in simplified Chinese, while in the United States, it is displayed in American English. If the traditional hard coding method is used, internationalization support cannot be achieved.

So generally speaking, internationalization is to configure a set of separate resource files for each language, which are saved in the project, and the system selects the appropriate resource files according to the needs of the client.

2. SpringBoot's support for Internationalization

Spring boot provides internationalization support by default, which is implemented by the automatic configuration class MessageSourceAutoConfiguration.

org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration

This class registers MessageSource to get internationalization configuration.

	@Bean
	public MessageSource messageSource() {
		MessageSourceProperties properties = messageSourceProperties();
		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;
	}

To implement native internationalization configuration in spring boot code, you only need the following three steps:

<1> Specify international resource path

Specify through application.properties:

spring.messages.basename=classpath:i18n/messages

Among them, i18n represents a folder on the resources path, and messages is the resource file name under this folder, for example: messages.properties, messages ABCD cn.properties, messages ABCD en ABCD us.properties, etc.

<2> Inject international Resolver object

Implement the internationalization policy by specifying the LocaleResolver object.

    @Bean
    public LocaleResolver localeResolver() {
        SessionLocaleResolver sessionLocaleResolver = new SessionLocaleResolver();
        sessionLocaleResolver.setDefaultLocale(Locale.CHINA);
        return sessionLocaleResolver;
    }

<3> use

In the resources directory, create the i18n folder. In the folder, create three files: messages.properties, messages [cn. Properties, messages [us. Properties. Add the required configuration.

At the same time, inject the MessageSource object into the Bean to be used, and use the getMessage method to

String getMessage(String code, @Nullable Object[] args, Locale locale) throws NoSuchMessageException;

3. Problems and shortcomings in the internationalization of springboot

Through the above three steps, we have realized the native internationalization of SpringBoot. However, in the process of use, we also found some shortcomings, mainly including:

<1> The configuration is stored in the jar package, which is not flexible enough.

<2> Most of the company's systems have been microserviced, and there are too many applications. I hope there is a place for unified management and configuration.

<3> It is hoped that the configuration reading is efficient and timely.

<4> In the internationalization work, the front and back end should not participate too much, and try to solve it at the framework level, rather than the application layer transmitting internationalization parameters.

4. The goal of spring boot internationalization enhancement

In practical work, we should and need to further enhance internationalization to make it more able to meet the requirements. Based on the above problems, we have made some improvements, and the final effect is as follows:

1. Configure the international configuration of the storage application in the center. The configuration supports dynamic refresh and takes effect in real time.

2. Realize efficient configuration reading.

3. Simplify the workload of front and back ends.

Therefore, the second half of this article will show how to enhance the internationalization of spring boot through Nacos.

2, Implementation process

1. Overall design

As required, we will store the configuration of the application service in the configuration center. Each time the client needs to obtain the configuration, the application service will go to the configuration center to pull the corresponding configuration, and finally return. The scheme is as follows:

However, the problem of this scheme is that every time the configuration is obtained, the application service needs to go to the configuration center to get the configuration, and every time the HTTP request needs to be sent, which is inefficient in performance. Therefore, improvements need to be made, as follows:

As shown in the figure above, compared with the original scheme, each time you get a configuration, you will first get it from the local cache of the service, and then get it from the configuration center if not. But how does the configuration work in real time? This requires further optimization of the above scheme. The final optimization scheme is as follows:

<1> When the application service starts, pull the configuration from the configuration center and store the configuration to the local cache.

<2> In the client acquisition configuration, the application service is directly obtained from the local cache.

<3> Configuration update event for the client subscription application service. When there is a configuration update in the configuration center, the configuration will be pushed to the application service, and the application service will update the local cache again.

<4> International configuration takes effect in near real time.

2. Implementation steps

<1> Nacos configuration

New namespace for prompt.

Add internationalization configuration of application on Nacos, select prompt for namespace, and Data ID is:

user-message.properties´╝îuser-message_zh_CN.properties´╝îuser-message_en_US.properties.

The configuration contents are:

User message. Properties: test = Test

User message ABCD CN. Properties: test = Test

user-message_en_US.properties: test=test

<2> New international configuration

Add the following internationalization configuration in application.yaml:

spring:  
  messages:
    baseFolder: i18n/
    basename: ${spring.application.name}-message
    encoding: UTF-8
    cacheMillis: 10000

The meaning of each field is as follows:

spring.messages.baseFolder: Specifies the local path (under the current path of the program) where the internationalization configuration is stored
spring.messages.basename: internationalization configuration name
spring.messages.encoding: encoding format
spring.messages.cacheMillis: time interval for international configuration refresh

<3> Code transformation

1 ~ international parser

A new custom internationalization parser, DefaultLocaleResolver, is added to parse the internationalization information of the request header. The code is as follows:

import org.apache.commons.lang3.StringUtils;
import org.springframework.web.servlet.LocaleResolver;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.Locale;

/**
 * Custom internationalization parser
 * @author zz
 * @date 2020/2/28 15:37
 **/
public class DefaultLocaleResolver implements LocaleResolver {

    @Override
    public Locale resolveLocale(HttpServletRequest request) {
        String lang = request.getHeader(LANG);
        Locale locale = Locale.getDefault();
        if (StringUtils.isNotBlank(lang)){
            String[] language = lang.split("_");
            locale = new Locale(language[0], language[1]);

            HttpSession session = request.getSession();
            session.setAttribute(LANG_SESSION, locale);
        }else{
            HttpSession session = request.getSession();
            Locale localeInSession = (Locale) session.getAttribute(LANG_SESSION);
            if (localeInSession != null){
                locale = localeInSession;
            }
        }
        return locale;
    }

    @Override
    public void setLocale(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Locale locale) {
    }

    /**
     * Request header field
     */
    private static final String LANG = "lang";

    /**
     * session
     */
    private static final String LANG_SESSION = "lang_session";

}

2. New international configuration

Read the internationalization configuration in the configuration file. The code is as follows:

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Component;

/**
 * International configuration
 * @author zz
 * @date 2020/2/28 15:38
 **/
@Data
@RefreshScope
@Component
@ConfigurationProperties(prefix = "spring.messages")
public class MessageConfig {

    /**
     * International file directory
     */
    private String baseFolder;

    /**
     * International file name
     */
    private String basename;

    /**
     * International Code
     */
    private String encoding;

    /**
     * Cache refresh time
     */
    private long cacheMillis;

}

3 ~ register international resolver and configure message resource manager

Add a new Spring configuration, register the custom internationalization parser DefaultLocaleResolver, and register ReloadableResourceBundleMessageSource to get the internationalization configuration in real time.

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.context.annotation.Primary;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.util.ResourceUtils;
import org.springframework.web.servlet.LocaleResolver;
import java.io.File;

/**
 * Spring To configure
 * @author zz
 * @date 2020/2/28 15:50
 **/
@Slf4j
@Configuration
public class SpringConfig {

    @Bean
    public LocaleResolver localeResolver(){
        return new DefaultLocaleResolver();
    }

    @Primary
    @Bean(name = "messageSource")
    @DependsOn(value = "messageConfig")
    public ReloadableResourceBundleMessageSource messageSource() {
        String path = ResourceUtils.FILE_URL_PREFIX + System.getProperty("user.dir") + File.separator + messageConfig.getBaseFolder() + File.separator + messageConfig.getBasename();
        log.info("International configuration content:{}", messageConfig);
        log.info("International configuration path:{}", path);
        ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
        messageSource.setBasename(path);
        messageSource.setDefaultEncoding(messageConfig.getEncoding());
        messageSource.setCacheMillis(messageConfig.getCacheMillis());
        return messageSource;
    }

    /**
     * apply name
     */
    @Value("${spring.application.name}")
    private String applicationName;

    @Autowired
    private MessageConfig messageConfig;

}

Note: here is a brief introduction to ReloadableResourceBundleMessageSource. ReloadableResourceBundleMessageSource is one of the two implementation classes of AbstractResourceBasedMessageSource. It provides a powerful function to refresh the configuration file regularly, supports the application to reload the configuration file without restarting, and ensures the long-term stable operation of the application. Therefore, we use it to realize the dynamic update of international information.

4 ~ add Nacos configuration manager

Add Nacos configuration manager, which mainly does two tasks: configuration pull and configuration update.

import com.alibaba.nacos.api.NacosFactory;
import com.alibaba.nacos.api.PropertyKeyConst;
import com.alibaba.nacos.api.config.ConfigService;
import com.alibaba.nacos.api.config.listener.Listener;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.stereotype.Component;
import java.io.File;
import java.io.IOException;
import java.util.Locale;
import java.util.Properties;
import java.util.concurrent.Executor;

/**
 * Nacos Configuration Manager
 * @author zz
 * @date 2020/2/28 16:30
 **/
@Slf4j
@Component
public class NacosConfig {


    @Autowired
    public void init() {
        serverAddr = applicationContext.getEnvironment().getProperty("spring.cloud.nacos.config.server-addr");
        dNamespace = applicationContext.getEnvironment().getProperty("spring.cloud.nacos.config.dNamespace");
        if (StringUtils.isEmpty(dNamespace)) {
            dNamespace = DEFAULT_NAMESPACE;
        }
        initTip(null);
        initTip(Locale.CHINA);
        initTip(Locale.US);
        log.info("Initialization of system parameters succeeded!apply name:{},Nacos address:{},Prompt namespace:{}", applicationName, serverAddr, dNamespace);
    }

    private void initTip(Locale locale) {
        String content = null;
        String dataId = null;
        ConfigService configService = null;
        try {
            if (locale == null) {
                dataId = messageConfig.getBasename() + ".properties";
            } else {
                dataId = messageConfig.getBasename() + "_" + locale.getLanguage() + "_" + locale.getCountry() + ".properties";
            }
            Properties properties = new Properties();
            properties.put(PropertyKeyConst.SERVER_ADDR, serverAddr);
            properties.put(PropertyKeyConst.NAMESPACE, dNamespace);
            configService = NacosFactory.createConfigService(properties);
            content = configService.getConfig(dataId, DEFAULT_GROUP, 5000);
            if (StringUtils.isEmpty(content)) {
                log.warn("Configuration content is empty,Skip initialization!dataId:{}", dataId);
                return;
            }
            log.info("Initialize internationalization configuration!Configuration content:{}", content);
            saveAsFileWriter(dataId, content);
            setListener(configService, dataId, locale);
        } catch (Exception e) {
            log.error("Initialization internationalization configuration exception!Abnormal information:{}", e);
        }
    }

    private void setListener(ConfigService configService, String dataId, Locale locale) throws com.alibaba.nacos.api.exception.NacosException {
        configService.addListener(dataId, DEFAULT_GROUP, new Listener() {
            @Override
            public void receiveConfigInfo(String configInfo) {
                log.info("New internationalization configuration received!Configuration content:{}", configInfo);
                try {
                    initTip(locale);
                } catch (Exception e) {
                    log.error("Initialization internationalization configuration exception!Abnormal information:{}", e);
                }
            }

            @Override
            public Executor getExecutor() {
                return null;
            }
        });
    }

    private void saveAsFileWriter(String fileName, String content) {
        String path = System.getProperty("user.dir") + File.separator + messageConfig.getBaseFolder();
        try {
            fileName = path + File.separator + fileName;
            File file = new File(fileName);
            FileUtils.writeStringToFile(file, content);
            log.info("Internationalization configuration updated!Local file path:{}", fileName);
        } catch (IOException e) {
            log.error("Initialization internationalization configuration exception!Local file path:{}Abnormal information:{}", fileName, e);
        }
    }

    /**
     * apply name
     */
    @Value("${spring.application.name}")
    private String applicationName;
    /**
     * Namespace
     */
    private String dNamespace;
    /**
     * server address
     */
    private String serverAddr;

    @Autowired
    private MessageConfig messageConfig;

    @Autowired
    private ConfigurableApplicationContext applicationContext;

    private static final String DEFAULT_GROUP = "DEFAULT_GROUP";

    private static final String DEFAULT_NAMESPACE = "515667c5-0450-4d1f-b14f-6f243079b6fb";

}

In this category, several things are mainly done:

First, when the application is started, the internationalization configuration is initialized by init method.

Second, in the init method, the Nacos server configuration will be pulled and written to the local cache. At the same time, a listener will be registered to monitor the configuration changes in real time and update the local cache in time.

Finally, the read namespace is specified through spring.cloud.nacos.config.dNamespace. If not, the default value is taken.

5 ~ new international configuration acquisition tool class

By injecting MessageSource, get the corresponding internationalization configuration through getMessage method.

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.context.NoSuchMessageException;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.stereotype.Component;
import java.util.Locale;

/**
 * International configuration get tool class
 * @author zz
 * @date 2020/2/28 17:00
 **/
@Slf4j
@Component
public class PropertiesTools {

    public String getProperties(String name) {
        try {
            Locale locale = LocaleContextHolder.getLocale();
            return messageSource.getMessage(name, null, locale);
        } catch (NoSuchMessageException e) {
            log.error("Get configuration exception!Abnormal information:{}", e);
        }
        return null;
    }

    @Autowired
    private MessageSource messageSource;

}

6~ use

By injecting PropertiesTools, call getProperties to get the internationalization configuration.

import com.demo.util.PropertiesTools;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * Service implementation example
 * @author zz
 * @date 2020/2/28 17:30
 **/
@Slf4j
@Service
public class DemoServiceImpl {

    @Override
    public String getProperties(String name) {
        return propertiesTools.getProperties(name);
    }

    @Autowired
    private PropertiesTools propertiesTools;

}

Three. Conclusion

After the above transformation, we have realized that the internationalization of spring boot has been enhanced through Nacos. Finally, let's make a simple summary.

1. The configuration file is managed through Nacos configuration center, updated in real time and effective in real time.

2. When reading the configuration file, the local cache file will be read to improve efficiency. After the configuration is updated, the server will push the configuration to the client, and the client will update the local configuration file.

3. The configuration file does not take effect in real time, which depends on spring.messages.cacheMillis (refresh interval).

Published 21 original articles, won praise 23, visited 10000+
Private letter follow

Posted by throx on Thu, 05 Mar 2020 19:32:04 -0800