SpringBoot RESTful 风格 API 多语言国际化i18n解决方案

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Mrqiang9001/article/details/86676207

1 摘要

随着互联网的迅速普及,目前(2019)国内的越来越多网络公司都开始走出国门,开辟海外市场,然而在出海之前需要解决的重大问题之一就是多语言环境。以往只有大型的跨国公司才会在项目中使用多语言国际化方案,现在这样的国际化应用将会越来越多。本文将介绍在 Spring Boot 项目中提供 RESTful 风格 API 的多语言国际化(i18n)解决方案

2 核心代码

2.1 多语言枚举类

将需要用到的语言添加到此类中

../demo-common/src/main/java/com/ljq/demo/springboot/common/i18n/LanguageEnum.java

package com.ljq.demo.springboot.common.i18n;

import lombok.Getter;
import lombok.ToString;
import org.springframework.util.StringUtils;

/**
 * @Description: 国际化 语言枚举类
 * @Author: junqiang.lu
 * @Date: 2019/1/24
 */
@Getter
@ToString
public enum LanguageEnum {

    /**
     * 美式英文
     */
    LANGUAGE_EN_US("en_us"),
    /**
     * 简体中文
     */
    LANGUAGE_ZH_CN("zh_cn");

    private String language;

    private LanguageEnum(String language){
        this.language = language;
    }

    /**
     * 获取指定语言类型(如果没有对应的语言类型,则返回中文)
     *
     * @param language 语言类型
     * @return
     */
    public static String getLanguageType(String language){
        if (StringUtils.isEmpty(language)) {
            return LANGUAGE_ZH_CN.language;
        }
        for (LanguageEnum languageEnum : LanguageEnum.values()) {
            if (languageEnum.language.equalsIgnoreCase(language)) {
                return languageEnum.language;
            }
        }
        return LANGUAGE_ZH_CN.language;
    }

}

2.2 多语言处理工具类

../demo-common/src/main/java/com/ljq/demo/springboot/common/i18n/I18nMessageUtil.java

package com.ljq.demo.springboot.common.i18n;

import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;

import java.io.IOException;

/**
 * @Description: 多语言国际化消息工具类
 * @Author: junqiang.lu
 * @Date: 2019/1/24
 */
public class I18nMessageUtil {

    private static MessageSourceAccessor accessor;
    private static MessageSource messageSource;
    private static final String PATH_PARENT = "classpath:i18n/";
    private static final String SUFFIX = ".properties";
    private static ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();

    private I18nMessageUtil(){
    }

    /**
     * 初始化资源文件的存储器
     * 加载指定语言配置文件
     *
     * @param language 语言类型(文件名即为语言类型,eg: en_us 表明使用 美式英文 语言配置)
     */
    private static void initMessageSourceAccessor(String language) throws IOException {
        /**
         * 获取配置文件名
         */
        Resource resource = resourcePatternResolver.getResource(PATH_PARENT + language + SUFFIX);
        String fileName = resource.getURL().toString();
        int lastIndex = fileName.lastIndexOf(".");
        String baseName = fileName.substring(0,lastIndex);
        /**
         * 读取配置文件
         */
        ReloadableResourceBundleMessageSource reloadableResourceBundleMessageSource = new ReloadableResourceBundleMessageSource();
        reloadableResourceBundleMessageSource.setBasename(baseName);
        reloadableResourceBundleMessageSource.setCacheSeconds(5);
        accessor = new MessageSourceAccessor(reloadableResourceBundleMessageSource);
        messageSource = reloadableResourceBundleMessageSource;
    }

    /**
     * 获取一条语言配置信息
     *
     * @param language 语言类型,zh_cn: 简体中文, en_us: 英文
     * @param message 配置信息属性名,eg: api.response.code.user.signUp
     * @param defaultMessage 默认信息,当无法从配置文件中读取到对应的配置信息时返回改信息
     * @return
     * @throws IOException
     */
    public static String getMessage(String language, String message, String defaultMessage) throws IOException {
        initMessageSourceAccessor(language);
        return accessor.getMessage(message,defaultMessage,LocaleContextHolder.getLocale());
    }
}

2.3 多语言的API返回状态码枚举类

../demo-common/src/main/java/com/ljq/demo/springboot/common/api/ResponseCodeI18n.java

package com.ljq.demo.springboot.common.api;

import lombok.Getter;
import lombok.ToString;

/**
 * @Description: 多语言,国际化接口返回码枚举
 * @Author: junqiang.lu
 * @Date: 2019/1/24
 */
@Getter
@ToString
public enum  ResponseCodeI18n {

    SUCCESS(1000, "api.response.code.success"),
    FAIL(-1, "api.response.code.fail"),

    // 公共参数
    PARAM_ERROR(1001, "api.response.code.paramError"),
    LANGUAGE_TYPE_ERROR(1002, "api.response.code.languageTypeError"),

    // 用户模块
    // 帐号
    ACCOUNT_NULL_ERROR(2001,"api.response.code.user.accountNullError"),
    ACCOUNT_FORMAT_ERROR(2002, "api.response.code.user.accountFormatError"),
    ACCOUNT_NOT_EXIST(2003,"api.response.code.user.accountNotExist"),
    ACCOUNT_EXIST(2004,"api.response.code.user.accountExist"),
    // 密码
    PASSWORD_NULL_ERROR(2101, "api.response.code.user.passwordNullError"),
    PASSWORD_FORMAT_ERROR(2102, "api.response.code.user.passwordFormatError"),
    PASSWORD_ERROR(2103,"api.response.code.user.passwordError"),


    UNKNOWN_ERROR(-1000,"api.response.code.unknownError");

    // 返回码
    private int code;

    // 返回信息
    private String msg;

    private ResponseCodeI18n(int code, String msg) {
        this.code = code;
        this.msg = msg;
    }
}

2.4 多语言 API 接口返回结果封装

../demo-common/src/main/java/com/ljq/demo/springboot/common/api/ApiResultI18n.java

package com.ljq.demo.springboot.common.api;

import com.ljq.demo.springboot.common.i18n.I18nMessageUtil;
import com.ljq.demo.springboot.common.i18n.LanguageEnum;
import lombok.Data;

import java.io.IOException;
import java.io.Serializable;

/**
 * @Description: 多语言, 国际化接口返回结果封装
 * @Author: junqiang.lu
 * @Date: 2019/1/24
 */
@Data
public class ApiResultI18n implements Serializable {

    private static final long serialVersionUID = 4518290031778225230L;

    /**
     * 返回码,1000 正常
     */
    private int code = 1000;

    /**
     * 返回信息
     */
    private String msg = "成功";

    /**
     * 返回数据
     */
    private Object data;

    /**
     * api 返回结果
     */
    private ApiResultI18n() {}

    /**
     * api 返回结果,区分多语言
     *
     * @param language 语言类型,eg: en_us 表示美式英文
     */
    public ApiResultI18n(String language){
        this.code = ResponseCodeI18n.SUCCESS.getCode();
        language = LanguageEnum.getLanguageType(language);
        try {
            this.msg = I18nMessageUtil.getMessage(language,ResponseCodeI18n.SUCCESS.getMsg(),"SUCCESS");
        } catch (IOException e) {
            this.msg = "SUCCESS";
        }
    }

    /**
     * 获取成功状态结果,区分多语言(默认简体中文)
     *
     * @param language 语言类型,eg: en_us 表示美式英文
     * @return
     */
    public static ApiResultI18n success(String language) {
        return success(null, language);
    }

    /**
     * 获取成功状态结果,区分多语言(默认简体中文)
     *
     * @param data 返回数据
     * @param language 语言类型,eg: en_us 表示美式英文
     * @return
     */
    public static ApiResultI18n success(Object data, String language) {
        ApiResultI18n result = new ApiResultI18n(language);
        result.setData(data);
        return result;
    }

    /**
     * 获取失败状态结果,区分多语言(默认简体中文)
     *
     * @param language 语言类型,eg: en_us 表示美式英文
     * @return
     */
    public static ApiResultI18n failure(String language) {
        return failure(ResponseCodeI18n.FAIL.getCode(), ResponseCodeI18n.FAIL.getMsg(), null, language);
    }

    /**
     * 获取失败结果,区分多语言(默认中文)
     *
     * @param responseCodeI18n 返回码
     * @param language 语言类型
     * @return
     */
    public static ApiResultI18n failure(ResponseCodeI18n responseCodeI18n, String language) {
        return failure(responseCodeI18n.getCode(), responseCodeI18n.getMsg(), null, language);
    }

    /**
     * 获取失败状态结果,区分多语言(默认中文)
     *
     * @param code 返回状态码
     * @param msg 错误信息
     * @param language 语言类型,eg: en 表示英文
     * @return
     */
    public static ApiResultI18n failure(int code, String msg, String language) {
        return failure(code ,msg, null, language);
    }

    /**
     * 获取失败返回结果,区分多语言(默认中文)
     *
     * @param code 错误码
     * @param msg 错误信息
     * @param data 返回结果
     * @param language 语言类型,eg: en 表示英文
     * @return
     */
    public static ApiResultI18n failure(int code, String msg, Object data, String language) {
        ApiResultI18n result = new ApiResultI18n(language);
        language = LanguageEnum.getLanguageType(language);
        try {
            msg = I18nMessageUtil.getMessage(language, msg, msg);
        } catch (IOException e) {
            msg = "Error";
        }
        result.setCode(code);
        result.setMsg(msg);
        result.setData(data);
        if (data instanceof String) {
            String m = (String) data;
            if (!m.matches("^.*error$")) {
                m += "error";
            }
        }
        return result;
    }
}

2.5 i18n 国际化多语言配置文件

语言的数量根据项目的需要,这里只列举类中文和英文两种语言

中文:

../demo-web/src/main/resources/i18n/zh_cn.properties

英文:

../demo-web/src/main/resources/i18n/en_us.properties

中文i18n多语言配置文件信息

# language = zh_cn(简体中文)

# api response code
api.response.code.success=成功
api.response.code.fail=失败
api.response.code.paramError=参数错误
api.response.code.languageTypeError=语言类型错误

# 用户模块
# 帐号
api.response.code.user.accountNullError=帐号为空
api.response.code.user.accountFormatError=账号(格式)错误
api.response.code.user.accountNotExist=账号不存在
api.response.code.user.accountExist=账号已经被注册
# 密码
api.response.code.user.passwordNullError=密码为空
api.response.code.user.passwordFormatError=密码格式错误
api.response.code.user.passwordError=密码错误


# 未知异常
api.response.code.unknownError=未知异常

英文的配置和中文格式一致,这里就不再列出

3 多语言 API 应用

项目配置了多语言之后,在接收参数的java bean 中需要添加语言字段,用于区分语言类型

语言类型字段属于一个公共字段,每一个接口都需要传递,建议写在一个基础公共类中,其他的参数接收类继承该类

3.1 参数接收 java bean

参数接收基础 bean

../demo-model/src/main/java/com/ljq/demo/springboot/BaseBean.java

package com.ljq.demo.springboot;

import lombok.Data;

import javax.validation.constraints.Pattern;
import java.io.Serializable;

/**
 * @Description: 基础 bean
 * @Author: junqiang.lu
 * @Date: 2018/12/24
 */
@Data
public class BaseBean implements Serializable {

    private static final long serialVersionUID = 6877955227522370690L;

    /**
     * 语言类型
     */
    @Pattern(regexp = "^\\w{2,10}$", message = "api.response.code.languageTypeError")
    private String language;

}

用户注册参数接收 bean

../demo-model/src/main/java/com/ljq/demo/springboot/vo/UserSignUpBean.java

package com.ljq.demo.springboot.vo;

import com.ljq.demo.springboot.BaseBean;
import lombok.Data;

import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;

/**
 * @Description: 用户注册接收参数bean
 * @Author: junqiang.lu
 * @Date: 2019/1/24
 */
@Data
public class UserSignUpBean extends BaseBean {

    /**
     * 账号
     */
    @NotNull(message = "api.response.code.user.accountNullError")
    @Pattern(regexp = "^\\S{5,50}$", message = "api.response.code.user.accountFormatError")
    private String userName;

    /**
     * 密码
     */
    @NotNull(message = "api.response.code.user.passwordNullError")
    @Pattern(regexp = "^\\w{5,80}$", message = "api.response.code.user.passwordFormatError")
    private String userPasscode;

}

3.2 DAO,Service,Controller 层

DAO 层基本不涉及到多语言,这里便不再贴出相关代码

Service 层

用户模块Service 接口

../demo-service/src/main/java/com/ljq/demo/springboot/service/UserService.java

package com.ljq.demo.springboot.service;

import com.ljq.demo.springboot.common.api.ApiResult;
import com.ljq.demo.springboot.common.api.ApiResultI18n;
import com.ljq.demo.springboot.vo.UserSignUpBean;

import java.util.Map;

/**
 * @Description: 用户模块业务
 * @Author: junqiang.lu
 * @Date: 2018/10/9
 */
public interface UserService {

 
    /**
     * 用户注册
     *
     * @param userSignUpBean 用户注册信息
     * @return
     */
    ApiResultI18n signUp(UserSignUpBean userSignUpBean) throws Exception;

}

用户模块 Service 实现类

../demo-service/src/main/java/com/ljq/demo/springboot/service/impl/UserServiceImpl.java

package com.ljq.demo.springboot.service.impl;

import com.ljq.demo.springboot.common.api.ApiResult;
import com.ljq.demo.springboot.common.api.ApiResultI18n;
import com.ljq.demo.springboot.common.api.ResponseCodeI18n;
import com.ljq.demo.springboot.dao.user.UserDao;
import com.ljq.demo.springboot.entity.UserDO;
import com.ljq.demo.springboot.service.UserService;
import com.ljq.demo.springboot.vo.UserSignUpBean;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.Map;

/**
 * @Description: 用户业务具体实现
 * @Author: junqiang.lu
 * @Date: 2018/10/9
 */
@Service("userService")
public class UserServiceImpl implements UserService {

    @Autowired
    private UserDao userDao;
    

    /**
     * 用户注册
     *
     * @param userSignUpBean 用户注册信息
     * @return
     */
    @Override
    public ApiResultI18n signUp(UserSignUpBean userSignUpBean) throws Exception{
        /**
         * 参数校验顺序: 基本入参校验 --> 具体参数合法性校验(非数据库层校验) --> 数据库层参数校验
         */

        /**
         * 请求参数获取
         */
        UserDO userParams = new UserDO();
        BeanUtils.copyProperties(userSignUpBean, userParams);
        /**
         * 此处省略
         * 参数合法性校验校验... ...
         */

        // 注册帐号数据库层校验
        int userCount = userDao.signUpCheck(userParams);
        if (userCount > 0) {
            return ApiResultI18n.failure(ResponseCodeI18n.ACCOUNT_EXIST, userSignUpBean.getLanguage());
        }
        /**
         * 此处省略
         * 用户注册相关操作... ...
         */

        return ApiResultI18n.success(userSignUpBean.getLanguage());
    }

}

用户模块 Controllerr 层

../demo-web/src/main/java/com/ljq/demo/springboot/web/controller/UserController.java

package com.ljq.demo.springboot.web.controller;

import com.ljq.demo.springboot.common.api.ApiResult;
import com.ljq.demo.springboot.common.api.ApiResultI18n;
import com.ljq.demo.springboot.common.api.ResponseCodeI18n;
import com.ljq.demo.springboot.common.exception.ParamsCheckException;
import com.ljq.demo.springboot.service.UserService;
import com.ljq.demo.springboot.vo.UserSignUpBean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.Map;

/**
 * @Description: 用户控制中心
 * @Author: junqiang.lu
 * @Date: 2018/10/9
 */
@RestController
@RequestMapping("api/user")
public class UserController {

    private static final Logger logger = LoggerFactory.getLogger(UserController.class);

    @Autowired
    private UserService userService;

   
    /**
     * 用户注册
     *
     * @param userSignUpBean 注册信息
     * @return
     */
    @RequestMapping(value = "signup", method = RequestMethod.POST)
    public ApiResultI18n signUp(@RequestBody UserSignUpBean userSignUpBean){
        ApiResultI18n apiResultI18n= null;
        try {
            apiResultI18n = userService.signUp(userSignUpBean);
        } catch (Exception e) {
            if (ParamsCheckException.class.isAssignableFrom(e.getClass())){
                logger.error("注册失败,参数错误");
                return apiResultI18n.failure(ResponseCodeI18n.PARAM_ERROR.getCode(), e.getMessage(),
                        userSignUpBean.getLanguage());
            }
            logger.error("注册失败,未知异常",e);
            return apiResultI18n.failure(ResponseCodeI18n.UNKNOWN_ERROR, userSignUpBean.getLanguage());
        }
        return apiResultI18n;
    }

}

4 测试

本地测试接口地址

http://127.0.0.1:8088/api/user/signup

中文参数

{
	"language" : "zh_cn",
	"userName" : "tom",
	"userPasscode" : "123456"
}

中文接口返回结果

{
    "code": 1001,
    "msg": "账号(格式)错误",
    "data": null
}

中文接口请求日志

2019-01-28 11:30:46:390 [http-nio-8088-exec-3] INFO  com.ljq.demo.springboot.web.acpect.LogAspect(LogAspect.java 66) -[AOP-LOG-START]
	requestMark: cc866c67-5fdc-4adb-855e-a79d67d31344
	requestIP: 127.0.0.1
	contentType:application/json
	requestUrl: http://127.0.0.1:8088/api/user/signup
	requestMethod: POST
	requestParams: {"language":"zh_cn","userName":"tom","userPasscode":"123456"}
	targetClassAndMethod: com.ljq.demo.springboot.web.controller.UserController#signUp
2019-01-28 11:30:46:405 [http-nio-8088-exec-3] ERROR c.l.demo.springboot.web.controller.UserController(UserController.java 91) -注册失败,参数错误
2019-01-28 11:30:46:407 [http-nio-8088-exec-3] INFO  com.ljq.demo.springboot.web.acpect.LogAspect(LogAspect.java 74) -[AOP-LOG-END]
	ApiResultI18n(code=1001, msg=账号(格式)错误, data=null)

英文参数

{
	"language" : "en_us",
	"userName" : "tom",
	"userPasscode" : "123456"
}

英文接口返回结果

{
    "code": 1001,
    "msg": "Account format error",
    "data": null
}

英文接口请求日志

2019-01-28 11:32:51:672 [http-nio-8088-exec-4] INFO  com.ljq.demo.springboot.web.acpect.LogAspect(LogAspect.java 66) -[AOP-LOG-START]
	requestMark: 756575c3-471a-4873-8f93-4850b3353038
	requestIP: 127.0.0.1
	contentType:application/json
	requestUrl: http://127.0.0.1:8088/api/user/signup
	requestMethod: POST
	requestParams: {"language":"en_us","userName":"tom","userPasscode":"123456"}
	targetClassAndMethod: com.ljq.demo.springboot.web.controller.UserController#signUp
2019-01-28 11:32:51:692 [http-nio-8088-exec-4] ERROR c.l.demo.springboot.web.controller.UserController(UserController.java 91) -注册失败,参数错误
2019-01-28 11:32:51:693 [http-nio-8088-exec-4] INFO  com.ljq.demo.springboot.web.acpect.LogAspect(LogAspect.java 74) -[AOP-LOG-END]
	ApiResultI18n(code=1001, msg=Account format error, data=null)

至此,一个 Spring boot RESTful 风格的多语言国际化 API 接口框架已经搭建完成

5 参考资料推荐

Spring 读取i18n国际化资源文件的工具类

自己动手在Spring-Boot上加强国际化功能

使rest的消息和内容国际化

Gtihub 源码地址 : https://github.com/Flying9001/springBootDemo
个人公众号:404Code,分享半个互联网人的技术与思考,感兴趣的可以关注.
404Code

猜你喜欢

转载自blog.csdn.net/Mrqiang9001/article/details/86676207