乐趣区

关于springcloud:基于SpringCloud的enum枚举值国际化处理实践

背景

选用 SpringCloud 框架搭建微服务做业务后盾利用时,会波及到大量的业务状态值定义,个别惯例做法是:

  • 长久层(数据库)存储 int 类型的值
  • 后盾零碎里用浏览性好一点儿的常量将 int 类型的值做一层映射
  • 前端 (app 或浏览器) 同样定义一套常量去映射这些关系
  • 前端调用后盾零碎的接口时,应用常量定义的 int 类型进行提交

源于长久层存储的优化规定,int 类型要比 varchar 类型效率高很多,这套做法也是大家接受度十分高的。

只是这里有一个不是很不便的中央:状态值映射的常量定义波及前端和后盾两局部,沟通的老本是一方面,另外如果状态值有变动,须要两组人员同时批改。

预期指标

在保障长久层的 int 类型存储状态值的前提下,次要是思考业务状态的可浏览性问题和多处批改的问题,可浏览性问题一部分能够通过前后端人员定义常量来解决,但接口调试时还是间接应用 int 类型,这部分的可浏览性问题还是存在,多处批改的问题须要重点解决。

本篇举荐的计划:

  • 长久层(数据库)存储没用原先的 int 类型值,这点放弃不变
  • 后盾零碎应用 enum 定义业务状态,不同的业务状态集能够由多个 enum 来实现,enum 反对国际化
  • 前端展现 enum 国际化的文本内容
  • 前端调用后盾零碎接口时,应用 enum 国际化的文本内容进行提交
  • 后盾接管 enum 国际化的文本内容转换成 int 类型值,存储在数据库

计划的长处:

  • 长久层原有的设计,效率性问题不受影响
  • 业务状态的定义、映射全副内聚到后盾零碎,后续有状态值变动时,只需后盾做相应批改即可
  • 前端展现的内容,接口传输的内容均为浏览性更好的文本,并且反对国际化

计划的毛病:

  • 后盾零碎存储、读取状态值时,须要用 enum 进行转换
  • 通信传输的内容报文比原有的 int 类型大一点点

计划实际

实际原理

此实际计划次要蕴含三局部:

  1. Enum 类应用 Jackson 进行 JSON 序列化和反序列化
  2. Enum 枚举项的 messages 国际化解决
  3. Enum 的定义

Enum 自定义序列化和反序列化

先定义 Enum 国际化类,自定义 Enum 的序列化和反序列化类,并应用注解 @JsonSerialize、@JsonDeserialize 注册到 Spring 的 ObjectMapper 中

@JsonDeserialize(using = DescEnumDeserializer.class)
@JsonSerialize(using = DescEnumSerializer.class)
public interface I18NEnum {

    /**
     * 获取枚举形容
     *
     * @return
     */
    String getDesc();}

参考一下自定义的序列化实现:

/**
 * @author huangying
 */
public class DescEnumSerializer extends JsonSerializer<I18NEnum> {

    @Override
    public void serialize(I18NEnum value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
        // 按类名 + 枚举值名称拼接配置文件 key,全副大写解决
        String key = value.getClass().getSimpleName() + "." + StringUtils.upperCase(value.toString());
        // I18NUtil 为国际化解决工具类
        String data = I18NUtil.get(key, value.getDesc());
        gen.writeString(data);
    }
}

自定义的反序列化实现:

/**
 * @author huangying
 */
public class DescEnumDeserializer extends JsonDeserializer<I18NEnum> {

    @Override
    public I18NEnum deserialize(JsonParser p, DeserializationContext ctx) throws IOException {JsonNode node = p.getCodec().readTree(p);
        Class enumCls = BeanUtils.findPropertyType(p.currentName(), p.getCurrentValue().getClass());
        List enumFields = EnumUtils.getEnumList(enumCls);
        String keyPrefix = enumCls.getSimpleName() + ".";
        for (Object enumField : enumFields) {I18NEnum i18NEnum = (I18NEnum) enumField;
            // I18NUtil 为国际化解决工具类
            String data = I18NUtil.get(keyPrefix + StringUtils.upperCase(i18NEnum.toString()), i18NEnum.getDesc());
            if (node.asText().equals(data)) {return i18NEnum;}
        }
        throw new I18NEnumException("enum: 未知的枚举类型");
    }
}

自定义一个专用异样,这样看起来更加高大上:

/**
 * @author huangying
 */
public class I18NEnumException extends RuntimeException {public I18NEnumException(String message) {super(message);
    }
}
国际化解决工具类

这个国际化解决的工具类是通用的,读取我的项目工程里的 messages.propertiesmessages_zh_CN.propertiesmessages_en.properties 等配置文件的 MessageSource 信息,并依据具体的语言,返回信息来实现国际化显示,代码如下:

/**
 * @author huangying
 */
@Component
public class I18NUtil {

    private static MessageSource messageSource;

    public I18NUtil(MessageSource messageSource) {I18NUtil.messageSource = messageSource;}

    public static String get(String key) {return messageSource.getMessage(key, null, LocaleContextHolder.getLocale());
    }

    public static String get(String key, Object arg) {return messageSource.getMessage(key, new Object[]{arg}, LocaleContextHolder.getLocale());
    }
}
Enum 定义示例

咱们举一个 enum 定义的示例,有 SUCCESS 和 FAIL 两个枚举值,存储在数据库中的 int 值别离是 1 和 2:

public enum OperateEnum implements I18NEnum {

    /**
     * 集体日常生产
     */
    SUCCESS(1, "SUCCESS"),
    /**
     * 装修
     */
    FAIL(2,"FAIL");

    private int index;
    private String desc;

    OperateEnum(int index, String desc) {
        this.index = index;
        this.desc = desc;
    }

    @Override
    public String getDesc() {return desc;}

    public int getIndex() {return index;}
}

配置文件的写法:

# messages.properties 内容
# 枚举类
OperateEnum.SUCCESS=success
OperateEnum.FAIL=fail

# messages_zh_CN.properties 内容
# 枚举类
OperateEnum.SUCCESS= 操作胜利
OperateEnum.FAIL= 操作失败

计划利用

在 SpringCloud 环境下,增加对国际化语言的解决,咱们对立将国内语言标识放在 request header 的 lang 外面:

/**
 * @author huangying
 */
public class I18NLocalResolver implements LocaleResolver {

    @Override
    public Locale resolveLocale(HttpServletRequest request) {String lang = request.getHeader("lang");
        // 获取 jvm 默认 locale
        Locale locale = Locale.getDefault();
        if (lang != null) {locale = new Locale(lang);
        }
        return locale;
    }

    @Override
    public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) {}}

自定义 enum 的序列化办法触发

在接口里只须要将 enum 类返回,在 @ResponseBody 进行解决时即可触发 enum 国际化的序列化办法,示例接口如下:

@ApiOperation(value = "枚举值国际化示例")
@ApiImplicitParams({@ApiImplicitParam(name = "uid", value = "操作人员 ID", paramType = "header", dataType = "Long")})
@RequestMapping(value = "/test/enums", method = RequestMethod.GET)
public Result get(@RequestHeader(value = "lang") String lang) {return Result.success(EnumUtils.getEnumList(OperateEnum.class));
}

自定义 enum 的反序列化办法触发

MappingJackson2HttpMessageConverter 转换器默认将 @RequestBody 的内容做反序列化解决,如果 enum 的国际化值传递给了客户端,若须要正确处理客户端提交的枚举值国际化内容,最简略的方法是将 enum 定义在 @RequestBody 的对象中,就能主动触发 enum 的自定义反序列化办法,并失去冀望的后果。

若在 @RequestParam 润饰的参数上定义 enum 对象,申请中的 String 转换成 enum 是通过 org.springframework.core.convert.support.StringToEnumConverterFactory 来实现的,该类实现了接口 ConverterFactory,通过调用 Enum.valueOf(Class, String) 实现了这个性能,而不会触发 enum 枚举值的反序列化。因而只能解决与枚举值雷同的字面值(name),enum 枚举值国际化解决后,可能与字面值不雷同,间接应用 @RequestParam 来转换,会报错。

如果要让 @RequestParam 可能触发 enum 枚举值的反序列化操作,能够尝试重写 springmvc 的参数转换器,此处略。

小结

enum 枚举值的国际化解决,是个十分有意思的改良,既可能解决浏览性的问题,又进步了业务定义的内聚性,此计划的利用取决于前后端的编码习惯,如果是在我的项目初期,前后端童鞋沟通确认后能够尝试此计划,心愿对你有帮忙。

专一 Java 高并发、分布式架构,更多技术干货分享与心得,请关注公众号:Java 架构社区
能够扫右边二维码增加好友,邀请你退出 Java 架构社区微信群独特探讨技术

退出移动版