java 使用注解自动转换字典信息

需求:项目开发过程中经常会将一些常量定义到字典中,但是页面展示需要将字典值转换成字典名称;
之前:提供字典查询接口,前段查询之后再回显;
现在:直接在需要转换的字段添加注解,自动添加转换后的字段

采用解决方案:在ResponseBody转换成json的时候处理添加自定义注解的字段;
对象属性上添加注入如:

// 自定义jackson 属性序列化注解,支持配置字典组名称或者使用枚举类型
@Dict
private String status;

使用Jackson 将对象转换成Json字符串时,会自动添加 statusDesc 字段;

{
    "name": "zhagnsan",
    "status": "enable",
    // 这个字段为字典转换自动生成的
    "statusDesc": "启动"
}

1. 自定义Jackson2ObjectMapperBuilder

自定义Jackson2ObjectMapperBuilder 修改jackson默认解析行为,添加自定义注解字段处理规则;

    @Bean
    public Jackson2ObjectMapperBuilder objectMapperBuilder() {
        Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
        SimpleModule simpleModule = new SimpleModule().setSerializerModifier(new DictSerializerModifier());
        builder.modules(simpleModule);
        return builder;
    }

2. 添加自定义注解处理类DictSerializerModifier

思路:继承Jdk8BeanSerializerModifier处理类,重写modifySerializer方法,实现自定义注解处理;

jackson中:ContextualSerializer说明

/**
 * Add-on interface that {@link JsonSerializer}s can implement to get a callback
 * that can be used to create contextual instances of serializer to use for
 * handling properties of supported type. This can be useful
 * for serializers that can be configured by annotations, or should otherwise
 * have differing behavior depending on what kind of property is being serialized.
 *<p>
 * Note that in cases where serializer needs both contextualization and
 * resolution -- that is, implements both this interface and {@link ResolvableSerializer}
 * -- resolution via {@link ResolvableSerializer} occurs first, and contextual
 * resolution (via this interface) later on.
 */
 public interface ContextualSerializer{
         public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property)
        throws JsonMappingException;
 }

大概意思:JsonSerializer 附加接口,主要用于序列化上下文实例,对于有注解的属性非常实用;特别注意需要配合ResolvableSerializer接口一起使用用于解析有注解的字段;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.TimeUnit;

/**
 * 字典序列化
 * 使用方式:
 * 在需要字典转换的javabean 属性上添加 @Dict 注解,则在使用Jackson生成json时,会自动添加已转换字典名称的属性  xxxDesc
 * 1. 需要扫描到 {@link ApplicationContextHolder};
 * 2. 需要注入{@link RedisTemplate}  客户端;
 * 3. 需要注入{@link DictionaryExport} 字典操作对象;
 * <pre>
 *     @Bean
 *     public Jackson2ObjectMapperBuilder objectMapperBuilder() {
 *         Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
 *         SimpleModule simpleModule = new SimpleModule().setSerializerModifier(new DictSerializerModifier());
 *         builder.modules(simpleModule);
 *         return builder;
 *     }
 * </pre>
 *
 * @author milin
 * @version V1.0
 * @date 2021/12/15 17:39
 */
@Slf4j
public class DictSerializerModifier extends BeanSerializerModifier {

    @Override
    public List<BeanPropertyWriter> changeProperties(SerializationConfig config, BeanDescription beanDesc, List<BeanPropertyWriter> beanProperties) {
        for (BeanPropertyWriter beanProperty : beanProperties) {
            Dict dict = beanProperty.getAnnotation(Dict.class);
            if (dict != null) {
                beanProperty.assignSerializer(new DictSerializer(dict));
            }
        }
        return beanProperties;
    }


    /**
     * 字典自定义序列化
     */
    static class DictSerializer extends JsonSerializer<Object> {
        /**
         * 生成序列化字段后缀
         */
        private static final String LABEL_SUFFIX = "Desc";
        /**
         * 字典配置信息
         */
        private final Dict dict;

        /**
         * 枚举获取key方法
         */
        private static final String[] KEY_METHODS = {"getValue", "getCode", "getStatus", "name"};
        /**
         * 获取枚举描述方法
         */
        private static final String[] DESC_METHODS = {"getDesc"};

        /**
         * 构造方法
         *
         * @param dict           字典描述
         * @param jsonSerializer 默认序列化对象
         */
        public DictSerializer(Dict dict) {
            this.dict = dict;
        }

        /**
         * Method that can be called to ask implementation to serialize
         * values of type this serializer handles.
         *
         * @param value       Value to serialize; can <b>not</b> be null.
         * @param gen         Generator used to output resulting Json content
         * @param serializers Provider that can be used to get serializers for
         *                    serializing Objects value contains, if any.
         */
        @Override
        public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
            provider.defaultSerializeValue(value, gen);
            // 添加转换之后的字段:xxxDesc
            String fieldName = gen.getOutputContext().getCurrentName();
            gen.writeStringField(fieldName.concat(LABEL_SUFFIX), value != null ? this.getDesc(dict, value) : null);
        }


        /**
         * 获取字典信息
         *
         * @param dict  字典对象
         * @param value 字典值
         * @return
         */
        private String getDesc(Dict dict, Object value) {
            try {
                // 先查询是否是枚举类型,查到则返回
                String enumDesc = this.getEnumDesc(dict, value);
                if (enumDesc != null) {
                    return enumDesc;
                }
                String valueStr = Objects.toString(value);
                //获取缓存key,可以自定义
                String key = DictConstants.getDictCacheKey(valueStr);
                // Redis 缓存操作类 这里建议优先使用本地缓存, 本地缓存 -> redis -> Db
                RedisTemplate redis = ApplicationContextHolder.getBean(RedisTemplate.class);
                if (redis.exists(key)) {
                    return redis.get(key);
                }
                // 数据库字典操作类
                DictionaryExport dictExport = ApplicationContextHolder.getBean(DictionaryExport.class);
                String desc = dictExport.selectNameByEnumKey(valueStr, dict.defaultValue());
                redis.setEx(key, desc, 1, TimeUnit.HOURS);
                return desc;
            } catch (Exception e) {
                log.error("字典转换:获取字典描述异常,使用默认值:{},key:{}, dict:{}, 异常:{}", dict.defaultValue(), value, dict.enumType(), e.getMessage(), e);
                return dict.defaultValue();
            }
        }


        /**
         * 获取枚举类型的描述信息
         *
         * @param dict  字典
         * @param value 值
         * @return 枚举desc字段
         */
        private String getEnumDesc(Dict dict, Object value) throws InvocationTargetException, IllegalAccessException {
            if (dict == null || value == null) {
                return null;
            }
            Class<? extends Enum<?>> et = dict.enumType();
            if (Dict.Void.class.equals(et)) {
                return null;
            }

            Enum<?>[] enums = et.getEnumConstants();
            Method keyMethod = this.getMethod(et, KEY_METHODS);
            if (keyMethod == null) {
                // 自定义业务异常
                throw new BusinessException(String.format("字典转换:枚举:%s,没有方法:%s", et.getName(), Arrays.toString(KEY_METHODS)));
            }
            Method descMethod = this.getMethod(et, DESC_METHODS);
            if (descMethod == null) {
                throw new BusinessException(String.format("字典转换:枚举:%s,没有方法:%s", et.getName(), Arrays.toString(DESC_METHODS)));
            }
            for (Enum<?> e : enums) {
                if (value.equals(keyMethod.invoke(e))) {
                    return Objects.toString(descMethod.invoke(e));
                }
            }
            log.error("字典转换:通过枚举转换失败,枚举:{},值:{},KeyMethod:{},DescMethod:{}", et.getName(), value, Arrays.toString(KEY_METHODS), Arrays.toString(DESC_METHODS));
            throw new BusinessException(String.format("字典转换失败,枚举:%s,值:%s", et.getName(), value));
        }

        /**
         * 获取读方法
         *
         * @param enumType    枚举类
         * @param methodNames 方法名称
         * @return Method
         */
        private Method getMethod(Class<? extends Enum<?>> enumType, String... methodNames) {
            for (String methodName : methodNames) {
                try {
                    return enumType.getMethod(methodName);
                } catch (NoSuchMethodException e) {
                }
            }
            return null;
        }
    }
}

/**
*字典常量
*/
public class DictConstants {
    /**
     * 字典缓存key前缀
     */
    public static final String JIM_DICT = "dict:enumKey_";

    /**
     * 获取字典缓存Key
     * @param enumKey
     * @return
     */
    public static String getDictCacheKey(String enumKey){
        return JIM_DICT + enumKey;
    }
}

有了上面的工具类配合自定义注解:@Dict 可以完成字典属性注入

3. 自定义注解

@Inherited
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Dict {
    static enum Void {}

    /**
     * 枚举类型的class
     * 取值:getValue, getCode, getStatus, name
     * 描述:getDesc
     *
     * @return 字典类型
     */
    Class<? extends Enum<?>> enumType() default Void.class;

    /**
     * 默认值,获取不到字典则使用默认值
     *
     * @return ""
     */
    String defaultValue() default "";
}

4. 使用自定义注解:


@Getter
@Setter
public class User{
    private String name;
    @Dict
    private String status;
}

使用上述自定义转换器会自动添加 ”statusDesc“ 字段,不需要手动添加字段;
另外也支持:枚举类型的自定义转换。
转换结果:

{
    "name": "zhagnsan",
    "status": "enable",
    "statusDesc": "启动"
}

(可选)在增加一个枚举转字典查询接口即可枚举和字典统一使用,再也不需要因为要回显需要在字典中添加枚举信息;

/**
 * 枚举类型的字典信息, 将枚举类型转换为字典列表类型
 *
 * @author milin
 * @version V1.0
 * @date 2021/12/16 11:23
 */
@Slf4j
public class DictEnumUtils {

    /**
     * 枚举配置信息
     */
    private static final Map<String, Class<? extends Enum<?>>> enumMap = new HashMap<>();
    /**
     * 枚举下拉列表缓存
     */
    private static final Map<String, List<DictionaryResponseDTO>> enumCache = new WeakHashMap<>();
    /**
     * 枚举获取key方法
     */
    private static final String[] KEY_METHODS = {"getValue", "getCode", "getStatus", "name"};
    /**
     * 获取枚举描述方法
     */
    private static final String[] DESC_METHODS = {"getDesc"};

    /**
     * 注册枚举类型,
     * 指定的枚举获取key方法:"getValue", "getCode", "getStatus", "name"
     * 获取Desc方法:getDesc
     *
     * @param type      枚举type
     * @param enumClass 对应枚举类
     */
    public static void registerEnum(String type, Class<? extends Enum<?>> enumClass) {
        enumMap.put(type, enumClass);
    }

    /**
     * 查询枚举类型的下拉列表
     *
     * @param type 请求枚举类型
     * @return 结果信息。转换失败会返回null
     */
    public static List<DictionaryResponseDTO> queryEnumDictionary(String type) {
        if (StringUtils.isBlank(type) || !enumMap.containsKey(type)) {
            return null;
        }
        if (enumCache.containsKey(type)) {
            return enumCache.get(type);
        }
        List<DictionaryResponseDTO> dictList = null;
        Class<? extends Enum<?>> enumClass = enumMap.get(type);
        try {
            Enum<?>[] enums = enumClass.getEnumConstants();
            Method keyMethod = getMethod(enumClass, KEY_METHODS);
            Method descMethod = getMethod(enumClass, DESC_METHODS);
            dictList = new ArrayList<>(enums.length);
            for (Enum<?> e : enums) {
                dictList.add(convert(type, keyMethod.invoke(e), descMethod.invoke(e), e.ordinal()));
            }
            enumCache.put(type, dictList);
        } catch (Exception e) {
            log.error("枚举转字典列表,类型:{},枚举:{},异常:{}", type, enumClass.getName(), e.getMessage(), e);
        }
        return dictList;
    }

    /**
     * 枚举转换到字典
     *
     * @return DictionaryResponseDTO 这个是自定义字典对象
     */
    private static DictionaryResponseDTO convert(String type, Object key, Object value, Integer ordinal) {
        DictionaryResponseDTO vo = new DictionaryResponseDTO();
        vo.setName(String.valueOf(value));
        vo.setEnumKey(String.valueOf(key));
        vo.setValue(ordinal);
        vo.setType(type);
        return vo;
    }


    /**
     * 获取读方法
     *
     * @param enumType    枚举类
     * @param methodNames 方法名称
     * @return Method
     */
    private static Method getMethod(Class<? extends Enum<?>> enumType, String... methodNames) throws NoSuchMethodException {
        for (String methodName : methodNames) {
            try {
                return enumType.getMethod(methodName);
            } catch (NoSuchMethodException e) {
            }
        }
        throw new NoSuchMethodException(String.format("枚举转字典列表,枚举:%s,没有找到方法:%s", enumType.getName(), Arrays.toString(methodNames)));
    }
}
Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐