自定义注解Dict,解决数据字典批量翻译繁琐的问题

概述

由于业务的需要,录入很多信息,而这些信息中绝大多数都是字典code,所以就会有很多数据字典的定义。
而前台数据的获取基本上都是下拉列表的选择。在保存或编辑的时候,返回到前端只要字典code就可以,但是在查看的时候,需要将字典翻译同时返回用于展示。

基于这样的前提下,受Hibernate Validator校验方式的启示,通过自定义注解的方式来实现。
这样做有一个非常大的优势:可拓展性强,以后新增字典项或字典类型直接加入字典仓库,然后在相关字段上声明就可以了。

实现的功能

由于公司项目的特殊性,存在老的字典码和新字典码映射转换;所以除了字典code的翻译,还有字典code的转换。
除了一对一的字典code的翻译,还有一种是上下级的翻译,比如省市区

  1. 一般字典code的翻译
  2. 上下级字典code的翻译
  3. 字典code的转换(主要是新老系统的兼容对接)

具体实现

话不多说,来看示例:

  1. 首先在DTO中添加自定义的注解
@Data
public class PersonDTO {
    /** 证件类型(字典) */
    private String custCertificateType;
    /** 学历(字典) */
    private String custEducation;
    /** 学位(字典) */
    private String custDegree;
    /** 单位所在省份(字典) */
    private String companyProvince;
    /** 单位所在城市(字典) */
    private String companyCity;
    /** 单位所在区/县(字典) */
    private String companyDistrict;

    /** 证件类型(名称) */
    @Dict(DICT_CERTIFICATE_TYPE)
    private String custCertificateTypeName;
    /** 学历(名称) */
    @Dict(DICT_EDUCATION)
    private String custEducationName;
    /** 学位(名称) */
    @Dict(DICT_DEGREE)
    private String custDegreeName;

    /** 单位所在省份(名称) */
    @Dict(value = DICT_AR_PROVINCE)
    private String companyProvinceName;
    /** 单位所在城市(名称) */
    @Dict(value = DICT_SYSTEM_AREA, isChild = true, parentField = "companyProvince")
    private String companyCityName;
    /** 单位所在区/县(名称) */
    @Dict(value = DICT_SYSTEM_AREA, isChild = true, parentField = "companyCity")
    private String companyDistrictName;
}

自定义注解Dict的定义如下:

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

    /** 数据字典code类型; 如果查找二级编码,则该值存储二级编码类型前缀,比如AR表示区域 */
    String value() default "";

    /** 存储编号的字段名称 如果是添加了Name后缀,则可省略*/
    String codeName() default "";

    /** 是否是子元素,如果是true,则 parentField不得为空*/
    boolean isChild() default false;

    /** 子元素的上级code字段名 */
    String parentField() default "";

    /** 当转换后的值为null的时候,是否进行赋值;默认为false,表示不覆盖 */
    boolean whetherSetIfNull() default false;

    /** 是否是转换编码;也就是说默认不转换code,而是翻译成name */
    boolean whetherConvertCode() default false;
}
  1. 其次是在接口输出list结果前调用service方法解析@Dict注解

使用注解的方式自定义解析,比较灵活。哪里需要使用,哪里调用方法解析就可以。

@Autowired private DictService dictService;

// 这里person从数据库查询出来的对象
PersonDTO person =  ……

// 数据字典翻译
dictService.translationDictCode(person);

// translationDictCode 方法的实现
@Override
public void translationDictCode(Object obj) {
    DictUtil.translationDictCode(obj, this::queryDictMapData, false);
}

// 其中DictUtil.translationDictCode方的定义如下:
 /**
     * 将DTO对象(对象中含有有Dict注解的对象)的字典码转换为字典值名称(注:通过Dict注解标识码值的字段)
     *
     * @param obj                处理的对象
     * @param convertMapFun      字典类型和值的集合转换为名称
     * @param whetherConvertCode 是否转换编码
     */
    public static void translationDictCode(final Object obj, Function<Map<String, String>, Map<String, String>> convertMapFun,
                                              boolean whetherConvertCode) 

// 这里的queryDictMapData定义如下:做为translationDictCode中的入参convertMapFun,它是接收一个map,然后返回另一个map,其中param是根据解析DTO上的Dict注解构造出来的查询参数。

/**
 * 查询字典码对应的name
 */
private Map<String, String> queryDictMapData(Map<String, String> param) {
    // 调用接口根据入参param查询数据字典翻译出来的name值
    // 一般数据字典放在redis缓存中,统一一个地方处理;这里可以根据这种入参去缓存中查询,然后封装返回需要格式的数据
// 主要就是解析入参,然后构建特定格式的返回值,比如:{"xx:certificateType=1": "身份证", "xx:certificateType=2": "护照"}
    return ……;
}

对queryDictMapData方法的入参和返回值的解读:

param 示例:{"relationship": "1,2", "AR:1001": "", ……}
其中key是字典type,value是要查询的字典值,有多个则用逗号分隔;
relationship对应一个字典项列表,比如[{"1": "朋友"}, {"2", "同事"}, {"3", "亲属"}……]
而value说明要查询字典code 1和2对应的翻译,那么返回结果如下:
因为字典type可能会有冒号字符,这里用=做链接符
{"relationship=1": "朋友", "relationship=2": "同事"}

  1. DictUtil的部分方法
    首先从方法translationDictCode一步步来剖析:

其实一开始是没有参数whetherConvertCode,后来为了兼容一种情况,就是先字典code的转换(老系统的code转换为新系统的code,反之亦然),然后再进行字典翻译

public static void translationDictCode(final Object obj, Function<Map<String, String>, Map<String, String>> convertMapFun,
                                              boolean whetherConvertCode) {
        if (Objects.isNull(obj)) {
            return;
        }

        Map<String, String> fieldKeyMap = new HashMap<>();
        Map<String, String> keyCodeMap = new HashMap<>();

        // 解析需要转换的字典码(递归处理)
        parseFieldCodeValue(obj, fieldKeyMap, keyCodeMap, whetherConvertCode);

        // 字典值转换(具体转换的方法)
        Map<String, String> convertedMap = convertMapFun.apply(keyCodeMap);

        // 回填字典码对应的值(递归处理)
        backFillCodeName(obj, fieldKeyMap, convertedMap, whetherConvertCode);
    }

除了对Object进行翻译,直接对List<Object>也是可以的,方法如下:

    /**
     * 将集合中所有对象的字典码转换为字典值 (注:通过Dict注解标识码值)
     *
     * @param list          处理的集合
     * @param convertMapFun 字典类型和值的集合转换为名称
     * @author liam
     */
    public static void translationListCodeName(List list, Function<Map<String, String>, Map<String, String>> convertMapFun) {
        if (null == list || !list.isEmpty()) {
            return;
        }

        Map<String, String> fieldKeyMap = new HashMap<>();
        Map<String, String> keyCodeMap = new HashMap<>();
        // 构造填充映射关系
        list.forEach(obj -> fillDictCodeConvertMap(obj, fieldKeyMap, keyCodeMap));
        // 字典值转换
        Map<String, String> convertedMap = convertMapFun.apply(keyCodeMap);
        // 给field赋值
        list.forEach(obj -> setObjAllFieldValue(obj, fieldKeyMap, convertedMap));
    }

完整的示例

  1. 测试类和运行结果
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Lists;

import java.util.List;

public class DictServiceTest {
    private static ObjectMapper objectMapper = new ObjectMapper();

    public static void main(String[] args) {
        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);

        DictService dictService = new DictService();

        PersonDTO person1 = new PersonDTO();
        person1.setCustCertificateType("1");
        PersonDTO person2 = new PersonDTO();
        person2.setCustCertificateType("2");
        person2.setCompanyProvince("10001");
        person2.setCompanyCity("1000101");
        List<PersonDTO> personList = Lists.newArrayList(person1, person2);

        System.out.println("转换之前");
        personList.forEach(DictServiceTest::printJson);

        // 字典翻译转换
        dictService.translationListCodeName(personList);

        System.out.println("转换之后");
        personList.forEach(DictServiceTest::printJson);
    }

    private static void printJson(Object obj) {
        try {
            System.out.println(objectMapper.writeValueAsString(obj));
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
    }
}

// 运行结果:
// 转换之前
// {"custCertificateType":"1"}
// {"custCertificateType":"2","companyProvince":"10001","companyCity":"1000101"}
// 转换之后
// {"custCertificateType":"1","custCertificateTypeName":"身份证"}
// {"custCertificateType":"2","companyProvince":"10001","companyCity":"1000101","custCertificateTypeName":"护照","companyProvinceName":"河北省","companyCityName":"邯郸市"}
  1. DTO和常量类
/**
 * 数据字典相关常量
 *
 * @Author liam
 */
public interface DictConstant {

    /** “=”等号*/
    String SIGN_EQUAL = "=";
    /** “:”冒号 */
    String SIGN_COLON = ":";
    /** “,”逗号*/
    String SIGN_COMMA = ",";

    /**XX系统接口的数据字典前缀*/
    String DICT_SYSTEM_XX = "XX";
    /**省市区数据字典前缀*/
    String DICT_SYSTEM_AREA = "AR";

    /** XX系统数据字典 */
    String DICT_XX_COLON = DICT_SYSTEM_XX + SIGN_COLON;
    /** 省市区数据字典 */
    String DICT_AR_COLON = DICT_SYSTEM_AREA + SIGN_COLON;

    /** 学历 */
    String DICT_EDUCATION = DICT_XX_COLON + "10001";
    /** 学位 */
    String DICT_DEGREE = DICT_XX_COLON + "10002";
    /** 证件类型 */
    String DICT_CERTIFICATE_TYPE = DICT_XX_COLON + "10003";

    /**省市区数据字典标识: 省*/
    String DICT_AR_PROVINCE = DICT_AR_COLON + "province";
}

import lombok.Data;
import static com.liam.dict.DictConstant.*;
@Data
public class PersonDTO {
    /** 证件类型(字典) */
    private String custCertificateType;
    /** 学历(字典) */
    private String custEducation;
    /** 学位(字典) */
    private String custDegree;
    /** 单位所在省份(字典) */
    private String companyProvince;
    /** 单位所在城市(字典) */
    private String companyCity;
    /** 单位所在区/县(字典) */
    private String companyDistrict;

    /** 证件类型(名称) */
    @Dict(DICT_CERTIFICATE_TYPE)
    private String custCertificateTypeName;
    /** 学历(名称) */
    @Dict(DICT_EDUCATION)
    private String custEducationName;
    /** 学位(名称) */
    @Dict(DICT_DEGREE)
    private String custDegreeName;

    /** 单位所在省份(名称) */
    @Dict(value = DICT_AR_PROVINCE)
    private String companyProvinceName;
    /** 单位所在城市(名称) */
    @Dict(value = DICT_SYSTEM_AREA, isChild = true, parentField = "companyProvince")
    private String companyCityName;
    /** 单位所在区/县(名称) */
    @Dict(value = DICT_SYSTEM_AREA, isChild = true, parentField = "companyCity")
    private String companyDistrictName;
}
  1. service类
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static com.liam.dict.DictConstant.*;

public class DictService {

    public void translationDictCode(Object obj) {
        DictUtil.translationDictCode(obj, this::queryDictMapData, false);
    }

    public void translationListCodeName(List list) {
        DictUtil.translationListCodeName(list, this::queryDictMapData);
    }

    /**
     * 查询字典码对应的name
     */
    private Map<String, String> queryDictMapData(Map<String, String> typeCodeMap) {

        // 调用接口根据入参param查询数据字典翻译出来的name值
        // 一般数据字典放在redis缓存中,统一一个地方处理;这里可以根据这种入参去缓存中查询,然后封装返回需要格式的数据

        Map<String, String> result = new HashMap<>();
        if (typeCodeMap.isEmpty()) {
            return result;
        }

        // 获取测试数据
        Map<String, List<DictDTO>> testData = getTestData();

        // 解析请求参数,构造返回结果
        typeCodeMap.forEach((key, value) -> {
            // 查询获取字典项列表
            List<DictDTO> dictList = testData.get(key);
            // 需要翻译的字典code
            String dictCode = typeCodeMap.get(key);
            if (dictCode.contains(SIGN_COMMA)) { // 逗号分隔
                Splitter.on(SIGN_COMMA)
                        .trimResults()
                        .splitToList(dictCode)
                        .forEach(code -> this.findSetCodeName(code, key, dictList, result));
            } else {
                this.findSetCodeName(dictCode, key, dictList, result);
            }
        });

        return result;
    }

    /**
     * 遍历取出所有值比较code是否相同,如果找到结束循环,取出结果
     *
     * @param dictCode : 字典code
     * @param hashKey : 字典码
     * @param list : 字典集合
     * @param result : 最终结果集
     */
    private void findSetCodeName(String dictCode, Object hashKey,
                                 List<DictDTO> list, Map<String, String> result) {
        for (DictDTO dto : list) {
            if (dto.getCode().equals(dictCode)) {
                result.put(hashKey + SIGN_EQUAL + dictCode, dto.getName());
                break;
            }
        }
    }
    /**
     * 获取测试数据
     */
    private Map<String, List<DictDTO>> getTestData() {
        return ImmutableMap.<String, List<DictDTO>>builder()
                .put(DICT_CERTIFICATE_TYPE, certificateTypeList)
                .put(DICT_AR_PROVINCE, provinceList)
                .put(DICT_AR_COLON + "10001", hebeiCityList)
                .build();
    }

    // 假如数据字典是如下列表

    /** 证件列表 */
    private List certificateTypeList = Lists.newArrayList(
        new DictDTO("1", "身份证"),
        new DictDTO("2", "护照"),
        new DictDTO("3", "驾驶证")
    );

    /** 省列表 */
    private List provinceList = Lists.newArrayList(
        new DictDTO("10001", "河北省"),
        new DictDTO("10002", "河南省"),
        new DictDTO("10003", "江苏省")
    );

    /** 河北市列表 */
    private List hebeiCityList = Lists.newArrayList(
        new DictDTO("1000101", "邯郸市"),
        new DictDTO("1000102", "秦皇岛市")
    );

    /** 临时测试数据 */
    private Map<String, String> demoTestData = ImmutableMap.<String, String>builder()
            .put(DICT_CERTIFICATE_TYPE + SIGN_EQUAL + "1", "身份证")
            .put(DICT_CERTIFICATE_TYPE + SIGN_EQUAL + "2", "护照")
            .put(DICT_AR_PROVINCE + SIGN_EQUAL + "10001", "上海市")
            .put(DICT_AR_COLON + "10001" + SIGN_EQUAL + "1000101", "浦东新区")
            .build();
}

@Data
@AllArgsConstructor
@NoArgsConstructor
public class DictDTO {
    /** 字典码 */
    private String code;
    /** 字典名称 */
    private String name;
}
  1. 工具类DictUtil,数据字典转换的核心
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanWrapperImpl;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.util.*;
import java.util.function.Consumer;
import java.util.function.Function;
import static com.liam.dict.DictConstant.*;
/**
 * 数据字典工具类
 *
 * @Author libing
 */
@Slf4j
public class DictUtil {

    /**
     * Name后缀,默认的字典名称字段命名规则
     */
    private static final String NAME_SUFFIX = "Name";

    /**
     * 将DTO对象(对象中含有有Dict注解的对象)的字典码转换为字典值名称(注:通过Dict注解标识码值的字段)
     *
     * @param obj                处理的对象
     * @param convertMapFun      字典类型和值的集合转换为名称
     * @param whetherConvertCode 是否转换编码
     */
    public static void translationDictCode(final Object obj, Function<Map<String, String>, Map<String, String>> convertMapFun,
                                           boolean whetherConvertCode) {
        if (Objects.isNull(obj)) {
            return;
        }

        Map<String, String> fieldKeyMap = new HashMap<>();
        Map<String, String> keyCodeMap = new HashMap<>();

        // 解析需要转换的字典码(递归处理)
        parseFieldCodeValue(obj, fieldKeyMap, keyCodeMap, whetherConvertCode);

        // 字典值转换(具体转换的方法)
        Map<String, String> convertedMap = convertMapFun.apply(keyCodeMap);

        /* 这里 keyCodeMap 、 fieldKeyMap 、 convertedMap 三者之间的关系
         *      keyCodeMap 是字典type与字典code值的关系
         *          比如: {"xx:certificateType": "1,2"}
         *     convertedMap 外部方法处理的结果,字典翻译的结果
         *          比如: {"xx:certificateType=1": "身份证", "xx:certificateType=2": "护照"}
         *     fieldKeyMap  是字典code所对应的字段与转换结果的关系
         *          比如: {"1:custCertificateType": "xx:certificateType=1", "2:custCertificateType": "xx:certificateType=2"}
         */

        // 回填字典码对应的值(递归处理)
        backFillCodeName(obj, fieldKeyMap, convertedMap, whetherConvertCode);
    }

    /**
     * 将集合中所有对象的字典码转换为字典值 (注:通过Dict注解标识码值)
     *
     * @param list          处理的集合
     * @param convertMapFun 字典类型和值的集合转换为名称
     * @author liam
     */
    public static void translationListCodeName(List list, Function<Map<String, String>, Map<String, String>> convertMapFun) {
        if (null == list || list.isEmpty()) {
            return;
        }

        Map<String, String> fieldKeyMap = new HashMap<>();
        Map<String, String> keyCodeMap = new HashMap<>();
        // 构造填充映射关系
        list.forEach(obj -> fillDictCodeConvertMap(obj, fieldKeyMap, keyCodeMap));
        // 字典值转换
        Map<String, String> convertedMap = convertMapFun.apply(keyCodeMap);
        // 给field赋值
        list.forEach(obj -> setObjAllFieldValue(obj, fieldKeyMap, convertedMap));
    }

    /**
     * 通过对Object的分析,构造填充2种转换关系map
     *
     * @param obj         : 取字段的对象
     * @param fieldKeyMap : (dictCode+字段名称)和接口查询返回的字典key的映射
     * @param keyCodeMap  : 用于接口查询的字典key和code的映射
     * @author liam
     */
    private static void fillDictCodeConvertMap(Object obj, Map<String, String> fieldKeyMap, Map<String, String> keyCodeMap) {
        if (null == obj) { // 如果为空,直接不做处理
            return;
        }

        Arrays.stream(obj.getClass().getDeclaredFields())
                .filter(field -> field.isAnnotationPresent(Dict.class))
                .forEach(field -> parseDictAnnotation(obj, field, fieldKeyMap, keyCodeMap));
    }

    /**
     * 解析需要转换的字典码(递归处理)
     *
     * @param obj                 : 取字段的对象
     * @param fieldKeyMap         : (dictCode+字段名称)和接口查询返回的字典key的映射
     * @param keyCodeMap          : 用于接口查询的字典key和code的映射
     * @param whetherConvertCode: 是否转换编码
     * @author liam
     */
    private static void parseFieldCodeValue(Object obj, Map<String, String> fieldKeyMap, Map<String, String> keyCodeMap,
                                            boolean whetherConvertCode) {
        if (Objects.isNull(obj)) {
            return;
        }

        // 获取当前对象所有的field
        Field[] declaredFields = obj.getClass().getDeclaredFields();

        // 构造填充映射关系
        Arrays.stream(declaredFields).forEach(field ->
                parseFieldObj(field, obj, whetherConvertCode,
                        fieldObj -> parseFieldCodeValue(fieldObj, fieldKeyMap, keyCodeMap, whetherConvertCode), // 递归处理
                        // 解析注解字段信息
                        () -> parseDictAnnotation(obj, field, fieldKeyMap, keyCodeMap)
                )
        );
    }

    /**
     * 回填字典码对应的值(递归处理)
     *
     * @param obj          : 回填字段的对象
     * @param fieldKeyMap  : (dictCode+字段名称)和接口查询返回的字典key的映射
     * @param convertedMap : 字典值转换后的map
     * @author liam
     */
    private static void backFillCodeName(Object obj, Map<String, String> fieldKeyMap, Map<String, String> convertedMap,
                                         boolean whetherConvertCode) {
        if (Objects.isNull(obj)) {
            return;
        }

        // 给field赋值
        Arrays.stream(obj.getClass().getDeclaredFields()).forEach(field ->
                parseFieldObj(field, obj, whetherConvertCode,
                        fieldObj -> backFillCodeName(fieldObj, fieldKeyMap, convertedMap, whetherConvertCode), // 递归处理
                        // 回填匹配的字段值
                        () -> setFieldValue(obj, field, fieldKeyMap, convertedMap)
                )
        );
    }

    /**
     * 解析field对象,对基本数据类型和复杂类型做不同的处理
     *
     * @param field               : 字段对象
     * @param obj                 : 字段所属的obj对象
     * @param recursiveFunc       : 递归处理方法
     * @param parseAnnotationFunc : 核心解析有注解字段的方法
     * @author liam
     */
    private static void parseFieldObj(Field field, Object obj, boolean whetherConvertCode,
                                      Consumer<Object> recursiveFunc,
                                      NestedFunction parseAnnotationFunc) {
        Class cls = field.getType();
        if (Map.class.isAssignableFrom(cls)) {
            return; // map直接跳过
        }
        // 需要数据字典转换的属性:有Dict注解
        if (field.isAnnotationPresent(Dict.class)
                // 且匹配whetherConvertCode的值
                && field.getAnnotation(Dict.class).whetherConvertCode() == whetherConvertCode) {
            parseAnnotationFunc.run(); // 核心方法处理,
        }
        // 没有注解的属性判断
        else {
            try {
                // 获取字段值且非空处理
                field.setAccessible(true);
                Optional.ofNullable(field.get(obj)).ifPresent(fieldValue -> {
                            // 集合类型,如果泛型的类型是JavaBean,继续递归处理
                            if (Collection.class.isAssignableFrom(cls)) {
                                // 如果是list-map结果,则这里返回null
                                Class generic = getGeneric(obj.getClass(), field.getName());
                                if (null != generic && notInFilterClass(generic)) {
                                    // 循环递归处理
                                    ((Collection) fieldValue).forEach(recursiveFunc::accept);
                                }
                            }
                            // 非基本数据类型
                            else if (notInFilterClass(cls)) {
                                recursiveFunc.accept(fieldValue);
                            }
                        }
                );

            } catch (Exception e) {
                log.error(e.getMessage(), e);
            }
        }
    }

    /**
     * 解析含有注解Dict的field信息,并将其充到2种转换关系map中
     *
     * @param obj         : 对象
     * @param field       : 字段
     * @param fieldKeyMap : (获取dictCode+字段名称)和接口查询返回的字典key的映射
     * @param keyCodeMap  : 用于接口查询的字典key和code的映射
     * @author liam
     */
    private static void parseDictAnnotation(Object obj, Field field,
                                            Map<String, String> fieldKeyMap,
                                            Map<String, String> keyCodeMap) {
        // 读取注解信息,获取编码类型
        Dict dict = field.getAnnotation(Dict.class);

        String fieldName = field.getName();

        // 根据Dict的codeName属性或者字段名称,获取字典编码code
        String code = getFieldValue(obj, dict, fieldName);
        if (!Strings.isNullOrEmpty(code)) {
            String dictType = dict.value();
            // 获取dictCode+字段名称的组合
            fieldName = getDictCodeCombineName(code, fieldName);
            // 二级编码字段
            if (dict.isChild()) {
                // 获取二级字段的字典编码值
                String parentCode = getPropertyValue(obj, dict.parentField());
                // 父级code
                String key = dictType + SIGN_COLON + parentCode;
                // 如果有相同的父级code,且code不重复
                if (null != keyCodeMap.get(key) && !code.equals(keyCodeMap.get(key))) {
                    keyCodeMap.put(key, keyCodeMap.get(key) + SIGN_COMMA + code);
                } else {
                    keyCodeMap.put(key, code);
                }
                // 属性名对应的 字典码和code
                fieldKeyMap.put(fieldName, key + SIGN_EQUAL + code);
            } else {
                // 有多个相同的类型,且code不重复
                if (null != keyCodeMap.get(dictType) && !code.equals(keyCodeMap.get(dictType))) {
                    keyCodeMap.put(dictType, keyCodeMap.get(dictType) + SIGN_COMMA + code);
                } else {
                    keyCodeMap.put(dictType, code);
                }
                // 属性名对应的 字典码和code
                fieldKeyMap.put(fieldName, dictType + SIGN_EQUAL + code);
            }
        }
    }

    /**
     * 给一个对象所有的含有Dict注解的字段赋值
     *
     * @param obj          : 取字段的对象
     * @param fieldKeyMap  : (dictCode+字段名称)和接口查询返回的字典key的映射
     * @param convertedMap : 字典值转换后的map
     * @author liam
     */
    private static void setObjAllFieldValue(Object obj, Map<String, String> fieldKeyMap, Map<String, String> convertedMap) {
        if (null == obj) { // 如果为空,直接不做处理
            return;
        }

        Arrays.stream(obj.getClass().getDeclaredFields())
                .filter(field -> field.isAnnotationPresent(Dict.class))
                .forEach(field -> setFieldValue(obj, field, fieldKeyMap, convertedMap));
    }

    /**
     * 给fiel设置值
     *
     * @param obj          : 对象
     * @param field        : 字段
     * @param fieldKeyMap  : 字段值映射关系
     * @param convertedMap :转换码关系
     * @author liam
     */
    private static void setFieldValue(Object obj, Field field, Map<String, String> fieldKeyMap,
                                      Map<String, String> convertedMap) {
        // 根据字段code和字段名称构建组合名称
        String fieldName = field.getName();
        Dict dictAnnotation = field.getAnnotation(Dict.class);
        String fieldValue = getFieldValue(obj, dictAnnotation, fieldName);
        String combineFieldName = getDictCodeCombineName(fieldValue, fieldName);

        Optional.ofNullable(fieldKeyMap.get(combineFieldName)).ifPresent(dicKey -> {
            String convertedValue = convertedMap.get(dicKey); // 转换后的值

            try {
                field.setAccessible(true);
                // 当转换后的值为null,判断是否需要将null值覆盖原有的值
                if (null == convertedValue) {
                    if (dictAnnotation.whetherSetIfNull()) {
                        field.set(obj, null);
                    }
                    return;
                }

                field.set(obj, convertedValue);
            } catch (Exception e) {
                log.error(e.getMessage(), e);
            }
        });
    }

    /**
     * 获取对象里指定属性的值,并转化为字符串
     *
     * @param obj          : 对象
     * @param propertyName : 对象里面的属性名称
     * @author liam
     */
    private static String getPropertyValue(Object obj, String propertyName) {
        BeanWrapperImpl beanWrapper = new BeanWrapperImpl(obj);
        if (beanWrapper.isReadableProperty(propertyName)) {
            Object propertyValue = beanWrapper.getPropertyValue(propertyName);
            if (null != propertyValue) {
                return propertyValue.toString();
            }
        }
        return "";
    }

    /**
     * 根据Dict的codeName属性或者字段名称,获取字段值 <br/>
     * <pre>
     *  注意:
     *     如果当前字段没有以Name结尾,那就取当前字段的值;也就是根据当前字段的值转换。
     * </pre>
     *
     * @param obj       : 对象
     * @param dict      : 字段注解对象
     * @param fieldName : 字段名称
     * @return java.lang.String
     * @author liam
     */
    private static String getFieldValue(Object obj, Dict dict, String fieldName) {
        String codeName = dict.codeName();
        if (Strings.isNullOrEmpty(codeName)) {
            // 如果当前字段是Name结尾,进行截取;否则取当前字段名称
            int endNameIndex = fieldName.lastIndexOf(NAME_SUFFIX);
            if (endNameIndex != -1) {
                codeName = fieldName.substring(0, endNameIndex);
            } else {
                codeName = fieldName;
            }
        }
        return getPropertyValue(obj, codeName);
    }

    /**
     * 获取dictCode+字段名称的组合
     *
     * @param dictCode  : 字段映射的字典码值
     * @param fieldName : 字段名称
     * @return dictCode:fieldName
     * @author liam
     */
    private static String getDictCodeCombineName(String dictCode, String fieldName) {
        // 为了解决list下的fieldName相同导致fieldKeyMap的key相同,添加dictCode唯一区分
        return dictCode + SIGN_COLON + fieldName;
    }

    /**
     * 获取一个类的属性的泛型;如果没有泛型,则返回null;
     * P.s 如果有多个,取第一个;如果有多层泛型,也返回null,比如List<Map>
     *
     * @param cls      :
     * @param property : 属性名
     * @author liam
     */
    public static Class getGeneric(Class cls, String property) {
        try {
            Type genericType = cls.getDeclaredField(property).getGenericType();
            // 如果是泛型参数的类型
            if (null != genericType && genericType instanceof ParameterizedType) {
                ParameterizedType pt = (ParameterizedType) genericType;
                Type type = pt.getActualTypeArguments()[0];
                // 这里,type也可能是 ParameterizedType, 直接不考虑
                if (type instanceof Class) {
                    return (Class) type;
                }
            }
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }
        return null;
    }

    /**
     * 判断不在过滤类(常用基本数据类型)中
     */
    private static boolean notInFilterClass(Class cls) {
        return !Lists.newArrayList(String.class, BigDecimal.class, Date.class,
                Integer.class, int.class,
                Double.class, double.class,
                Boolean.class, boolean.class,
                Long.class, long.class,
                Character.class, char.class)
                .contains(cls);
    }

    /**
     * 函数式接口:类似freemarker中的<#nested>处理
     */
    @FunctionalInterface
    public interface NestedFunction {
        /**
         * 无参无返回值的方法运行
         */
        void run();
    }
}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容