优雅的实现字典翻译

李同学 李同学 | 362 | 2023-03-28

当我们在Java应用程序中需要对字典属性进行转换返回给前端时,如何简单、方便、并且优雅的处理是一个重要问题。在本文中,我们将介绍如何使用Java中的序列化机制来优雅地实现字典值的翻译,从而简化开发。

什么是序列化

在Java中,序列化是将对象转换为字节流的过程,可以将这些字节流保存到文件中或通过网络进行传输。反序列化是将字节流转换为原始对象的过程。通过序列化和反序列化,我们可以在不同的应用程序之间传递对象,也可以将对象保存到文件中以便以后使用。

使用序列化实现字典值的翻译

在Java中,我们可以使用序列化机制来实现编码与其对应的含义的对应关系。具体步骤如下:

  1. 定义一个字典注解与,例如:

    @Target({ElementType.FIELD})
    @Retention(RetentionPolicy.RUNTIME)
    @JacksonAnnotationsInside
    @JsonSerialize(using = DictSerializer.class)
    public @interface Dict {
    ​
        /**
         * 字典类型
         * 比如在描述学生的时候,1代表小学生 2代表初中生 3代表高中生 4代表大学生
         * 同样在描述老师的时候,1代表语文老师 2代表数学老师 3代表英语老师 4代表体育老师
         * 同样的数值在不同类型下,代表含义不同,所以需要指定字典的类型
         */
        String dic();
    }
    
  2. 自定义注解结合继承JsonSerialize实现ContextualSerializer,实现返回结果转译:

    @Slf4j
    public class DictSerializer extends StdSerializer<Object> implements ContextualSerializer {
    ​
        private transient String dictCode;
    ​
        @Override
        public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty beanProperty){
            Dict dict = beanProperty.getAnnotation(Dict.class);
            return createContextual(dict.dic());
        }
    ​
        private JsonSerializer<?> createContextual(String dicCode) {
            DictSerializer serializer = new DictSerializer();
            serializer.setDictCode(dicCode);
            return serializer;
        }
    ​
        @Override
        public void serialize(Object value, JsonGenerator gen, SerializerProvider provider){
    ​
            String dictCode = getDictCode();
            if (StrUtil.isBlank(dictCode)) {
                return;
            }
            if (Objects.isNull(value)) {
                return;
            }
            try {
                // 因为序列化是每个对象都需要进行序列话操作,这里为了减少网络IO,使用了 guava 的本地缓存(代码在下面)
                Map<String, String> dictMap = DictionaryConstants.DICTIONARY_CACHE.get(dictCode);
                if (dictMap.containsKey("nullValue")) {
                    // 当本地缓存中不存在该类型的字典时,就调用查询方法,并且放入到本地缓存中(代码在下面)
                    dictMap = translateDictValue(dictCode);
                    DictionaryConstants.DICTIONARY_CACHE.put(dictCode, dictMap);
                }
                // 通过数据字典类型和value获取name
                String label = dictMap.get(value.toString());
                gen.writeObject(value);
                // 在需要转换的字段上添加@Dict注解,注明需要引用的code,后端会在返回值中增加filedName_dictText的key,前端只需要取对应的 filedName_dictText 就可以直接使用
                gen.writeFieldName(gen.getOutputContext().getCurrentName() + DictionaryConstants.DICT_TEXT_SUFFIX);
                gen.writeObject(label);
            } catch (Exception e) {
                log.error("错误信息:{}", e.getMessage(), e);
            }
        }
    ​
        private String getDictCode() {
            return dictCode;
        }
    ​
        private void setDictCode(String dictCode) {
            this.dictCode = dictCode;
        }
    ​
        protected DictSerializer() {
            super(Object.class);
        }
    }
    
  3. 将同类型的字典编码和对应的含义保存到一个Map中,例如:

    private Map<String, String> translateDictValue(String code) {
        if (StrUtil.isBlank(code)) {
          return null;
        }
        // Map<String, String> map = new HashMap<>();
        // map.put("1", "小学生");
        // map.put("2", "初中生");
        // map.put("3", "高中生");
        // map.put("4", "大学生");
      
        // 因为我们公司采用微服务,然后字典模块单独拆分成一个服务,所以这里使用Feign方式调用
        DictionaryFeignClient dictionaryFeign = SpringUtil.getBean(DictionaryFeignClient.class);
        return dictionaryFeign.dictionary(code);
    }
    
  4. 因为序列化是需要每个对象都进行序列话操作,如果返回的是集合的话,就会进行很多次序列化操作,此时就需要对相同类型的字典进行缓存,我这里使用了guava 的 LoadingCache 进行本地缓存(这里可能有人会说了,如果这个时候字典值对应的含义修改了,你这个缓存岂不是会导致数据不正确,首先字典功能一般是管理端进行增删改操作,而且字典一旦定好了是不会轻易修改的,如果你要硬杠,你赢了)。

    public class DictionaryConstants {
    ​
        /**
         * 字典翻译文本后缀
         */
        public static final String DICT_TEXT_SUFFIX = "_dictText";
    ​
        public static final LoadingCache<String, Map<String, String>> DICTIONARY_CACHE = CacheBuilder.newBuilder()
                .maximumSize(1000)
                .expireAfterWrite(30, TimeUnit.SECONDS)
                .expireAfterAccess(10, TimeUnit.SECONDS)
                .build(new CacheLoader<String, Map<String, String>>() {
                    @Override
                    public Map<String, String> load(String key) {
                        Map<String, String> map = new HashMap<>();
                        map.put("nullValue", "nullValue");
                        return map;
                    }
                });
    }
    

    这里额外补充一个小知识:

    • expireAfterWrite和expireAfterAccess都是Google Guava缓存库中的缓存过期策略。
    • expireAfterWrite表示缓存项在指定时间后过期,无论缓存项是否被访问过。例如,如果我们将缓存项的expireAfterWrite设置为10分钟,则缓存项在被添加到缓存中10分钟后过期,无论它是否被访问过。
    • 这两种过期策略可以单独或组合使用,以实现更灵活的缓存策略。例如,我们可以将缓存项的expireAfterWrite设置为10分钟,同时将expireAfterAccess设置为5分钟,这样缓存项将在10分钟后过期,或者在最近5分钟内没有被访问时过期,以先到者为准。
    • 使用expireAfterWrite和expireAfterAccess可以避免缓存中的数据过期时间过长或过短,从而提高缓存的效率和可靠性。
  5. 相比于使用 aop 切面的方式,使用序列化的方式能更好的进行字典的翻译(因为 aop 方式很难处理对象中的属性的属性),例如:

    public class Company {
      private List<Staff> staffs;
    }
    ​
    public class Staff {
      private Integer age;
      private String name;
      @Dic(dic = "position")
      private String position;
      
    }
    

    在这种场景中,如果返回的是 Company 集合,使用 aop 切面方式就很难达到(开发难度与开发成本)与序列化方式同样的效果。

通过以上步骤,我们可以使用Java中的序列化机制来优雅地实现字典编码与其对应的含义的对应关系,从而简化编码数据的管理和维护。

总结

在本文中,我们介绍了如何使用Java中的序列化机制来实现编码与其对应的含义的对应关系,从而简化编码数据的管理和维护。通过序列化和反序列化,我们可以高效地传递和保存对象,提高应用程序的效率和可维护性。

文章标签: Java
推荐指数:

真诚点赞 诚不我欺~

优雅的实现字典翻译

点赞 收藏 评论

关于作者

李同学
李同学

这个人很懒~

等级 LV1

粉丝 0

获赞 1

经验 12