Spring Boot国际化


让国际友人也能快乐的访问的你的服务

spring-boot-starter-web里面有国际化需要的API,因此我们不需要引入其他的API了。

定义国际化的配置文件

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver;

import java.util.Locale;

/**
 * @author fangxi
 */
@Configuration
public class I18nConfiguration {

    /**
     * Accept-Language Header 区域解析器
     */
    @Bean
    public LocaleResolver localeResolver() {
        AcceptHeaderLocaleResolver acceptHeaderLocaleResolver = new AcceptHeaderLocaleResolver();
        acceptHeaderLocaleResolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
        return acceptHeaderLocaleResolver;
    }

    /**
     * 加载国际化配置文件资源
     */
    @Bean
    public ReloadableResourceBundleMessageSource messageSource() {
        ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
        //设置国际化配置文件存放目录
        messageSource.setBasename("classpath:i18n/messages");
        //设置加载资源的缓存失效时间,-1表示永久有效,默认为-1
        messageSource.setCacheSeconds(-1);
        //设定 Resource Bundle 编码方式,默认为UTF-8
        messageSource.setDefaultEncoding("UTF-8");
        return messageSource;
    }
}

AcceptHeaderLocaleResolver:Spring内置的国际化处理,获取Http请求头中的Accept-Language属性,判断当前环境是哪种语言。

resources目录下,创建i18n目录和两个国际化的文件,要以message开头

messages_en_US.properties,英文提示

delete.success=delete success.
save.success=Save Success.
update.success=Update Success.

messages_zh_CN.properties,中文提示

delete.success=删除成功
save.success=保存成功
update.success=更新成功

定义一个工具类,根据当前语言环境和国际化提示中的key获取对应的提示

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.stereotype.Component;

import java.util.Locale;

/**
 * @author fangxi
 */
@Component
public class I18nUtils {

    private static ReloadableResourceBundleMessageSource messageSource;

    @Autowired
    I18nUtils(ReloadableResourceBundleMessageSource messageSource) {
        I18nUtils.messageSource = messageSource;
    }

    public static String toLocale(String message) {
        Locale locale = LocaleContextHolder.getLocale();
        return messageSource.getMessage(message, null, locale);
    }

}

为了统一管理key,我们用枚举保存。

import lombok.AllArgsConstructor;

import java.util.stream.Stream;

/**
 * @author fangxi
 */
@AllArgsConstructor
public enum I18nEnum {

    SAVE_SUCCESS("save.success"),
    UPDATE_SUCCESS("update.success"),
    DELETE_SUCCESS("delete.success"),
    ;

    private final String message;

    public String message() {
        return this.message;
    }

    public static boolean exists(String s) {
        return Stream.of(I18nEnum.values())
                .map(I18nEnum::message)
                .anyMatch(s::equals);
    }

}

注:用枚举是因为我们在全局异常处理的时候,要获遍历枚举里面的常量,从而判断是否要国际化。

使用

@PostMapping
public ResponseEntity<String> save(@RequestBody @Validated UserRequestVO requestVO) {
    userService.save(requestVO);
    return ResponseEntity.status(HttpStatus.CREATED)
        .body(I18nUtils.toLocale(I18nEnum.SAVE_SUCCESS.message()));
}

这样一来,会就完成了提示消息国际化功能。

上面的做法不能让入参校验进行国际化,比如 @NotNull(message = "请传入xxxx")。因此,我们还需要改造,定义两组提示信息

messages_zh_CN.properties

paging.tip=请传入分页参数

messages_en_US.properties

paging.tip=Please pass in paging parameters

改造注解上的提示信息:@NotNull(message = "paging.tip")

import lombok.Data;

import javax.validation.constraints.NotNull;

/**
 * @author fangxi
 */
@Data
public class PageRequestVO {

    @NotNull(message = "paging.tip")
    private Integer page;

    @NotNull(message = "paging.tip")
    private Integer size;

}

现在一般提交数据的方式用两种:表单提交和JSON格式

如果是表单提交,校验不通过的话,会抛出BindException。JSON格式的提交,校验不通过则会抛出MethodArgumentNotValidException。所以,我们要拦截这两种异常处理

import com.isecstar.print.common.enums.I18nEnum;
import com.isecstar.print.common.utils.I18nUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import javax.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * @author fangxi
 */
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Map<String, Object> methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException methodArgumentNotValidException, HttpServletRequest request) {
        return getError(methodArgumentNotValidException, request);
    }

    @ExceptionHandler(BindException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Map<String, Object> bindException(BindException bindException, HttpServletRequest request) {
        return getError(bindException, request);
    }

    private Map<String, Object> getError(Exception e, HttpServletRequest request) {
        log.error("", e);
        Map<String, Object> error = new HashMap<>(5);
        String message = e.getMessage();
        error.put("message", I18nEnum.exists(message) ? I18nUtils.toLocale(message) : message);
        error.put("code", HttpStatus.INTERNAL_SERVER_ERROR.value());
        if (e instanceof MethodArgumentNotValidException) {
            MethodArgumentNotValidException methodArgumentNotValidException = (MethodArgumentNotValidException) e;
            BindingResult bindingResult = methodArgumentNotValidException.getBindingResult();
            // messageList就是写在@NotNull(message = "")message的信息, 这里获取到的是 paging.tip
            Set<String> messageList = bindingResult.getFieldErrors().stream().map(FieldError::getDefaultMessage).collect(Collectors.toSet());
            List<String> globalizationMessage = messageList.stream().map(I18nUtils::toLocale).collect(Collectors.toList());
            error.put("message", globalizationMessage);
            error.put("httpStatus", HttpStatus.BAD_REQUEST);
            error.put("code", HttpStatus.BAD_REQUEST.value());
        } else if (e instanceof BindException) {
            BindException bindException = (BindException) e;
            // messageList就是写在@NotNull(message = "")message的信息, 这里获取到的是 paging.tip
            Set<String> messageList = bindException.getBindingResult().getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.toSet());
            List<String> globalizationMessage = messageList.stream().map(I18nUtils::toLocale).collect(Collectors.toList());
            error.put("message", globalizationMessage);
            error.put("httpStatus", HttpStatus.BAD_REQUEST);
            error.put("code", HttpStatus.BAD_REQUEST.value());
        }
        error.put("path", request.getRequestURI());
        error.put("timestamp", LocalDateTime.now());
        return error;
    }
}

这样一来,我们就可以在校验中,对外国际化之后的异常信息。


Author: Re:0
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint policy. If reproduced, please indicate source Re:0 !
 Previous
PropertyMapper使用 PropertyMapper使用
PropertyMapper是Spring提供的一个工具类,主要用于重新赋值,转换等操作,位于org.springframework.boot.context.properties.PropertyMapper下。
2022-03-01
Current 
Spring Boot国际化 Spring Boot国际化
让国际友人也能快乐的访问的你的服务 spring-boot-starter-web里面有国际化需要的API,因此我们不需要引入其他的API了。
2022-02-26
  TOC