【Java】将枚举类转换为Redis字典缓存

 

字典翻译框架实现看这篇:

https://www.cnblogs.com/mindzone/p/16890632.html

  

枚举的特性

首先是枚举的一些特性:

1、枚举实例直接在枚举类中声明

2、重载构造器,可以直观表示枚举的属性信息

3、枚举类的方法:

  - values 可以获取枚举类的所有枚举项(数组)

  - 枚举实例 ordinal() 方法获取枚举的下标值

  - 枚举实例 name() 方法 获取你声明的实例名

4、枚举是完全单例的

5、枚举不可以作为通常对象响应给Web传数据,必须自己提取信息转换

package cn.hyite.amerp.common.state;

import cn.hyite.amerp.common.state.intf.IEnumStateConvert;
import lombok.Getter;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 盖印状态枚举类
 * @author OnCloud9
 * @version 1.0
 * @project amerp-server
 * @date 2022年11月23日 13:31
 */
@Getter
public enum StampStateEnum implements IEnumStateConvert {
    UNUSED("未盖印", "0"),
    DONE("已盖印", "1"),
    CANCEL("已取消", "2"),
    ;

    public static final String CATE = "STAMP_STATE";
    private final String code;
    private final String name;

    StampStateEnum(String name, String code) {
        this.name = name;
        this.code = code;
    }

    @SuppressWarnings("all")
    @Override
    public Map<String, String> getItem() {
        Map<String, String> instance = new ConcurrentHashMap<>();
        instance.put("instName", name());
        instance.put("instIndex", String.valueOf(ordinal()));
        instance.put("name", name);
        instance.put("code", code);
        return instance;
    }
}

  

基于枚举,我们可以设置字典的属性,相比普通类更直观的表现【字典】

    UNUSED("未盖印", "0"),
    DONE("已盖印", "1"),
    CANCEL("已取消", "2"),

  

我最初的设计几乎和DictDTO没区别,关键字段是 字典编号,字典名称,字典类别

存储方式就直接在应用中,不需要Redis,缓存的抽象设计沿用Redis的方案

package cn.cloud9.server.test.model;

import lombok.AllArgsConstructor;
import lombok.Getter;
import org.apache.commons.collections.CollectionUtils;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
 * @author OnCloud9
 * @description
 * @project tt-server
 * @date 2022年11月06日 下午 06:25
 */
@Getter
@AllArgsConstructor
public enum MyType {

    GROUP1_TYPE1("a类型1", 1001, MyType.KEY_GP1),
    GROUP1_TYPE2("a类型2", 1002, MyType.KEY_GP1),
    GROUP1_TYPE3("a类型3", 1003, MyType.KEY_GP1),

    GROUP2_TYPE1("b类型1", 1001,  MyType.KEY_GP2),
    GROUP2_TYPE2("b类型2", 1002,  MyType.KEY_GP2),
    GROUP2_TYPE3("b类型3", 1003,  MyType.KEY_GP2),

    GROUP3_TYPE1("c类型1", 1001, MyType.KEY_GP3),
    GROUP3_TYPE2("c类型2", 1002, MyType.KEY_GP3),
    GROUP3_TYPE3("c类型3", 1003, MyType.KEY_GP3),
    ;

    private final String name;
    private final Integer value;
    private final String cate;

    private static final Map<String, List<MyType>> typeList = new ConcurrentHashMap<>();
    private static final Map<String, String> typeMap = new ConcurrentHashMap<>();

    public static final String SEPARATOR = "@";
    public static final String KEY_GP1 = "GT-1001";
    public static final String KEY_GP2 = "GT-1002";
    public static final String KEY_GP3 = "GT-1003";

    static {
        for (MyType myType : MyType.values()) {
            final String myTypeCate = myType.getCate();
            final String myTypeName = myType.getName();
            final Integer myTypeValue = myType.getValue();

            final String key = myTypeCate + SEPARATOR + myTypeValue;
            typeMap.put(key, myTypeName);

            List<MyType> myTypes = typeList.get(myTypeCate);
            if (CollectionUtils.isEmpty(myTypes)) {
                myTypes = new ArrayList<>();
                typeList.put(myTypeCate, myTypes);
            }
            myTypes.add(myType);
        }
    }

    /**
     * 按枚举类别获取枚举集合
     * @param cate
     * @return
     */
    public static List<MyType> getTypeListByCate(String cate) {
        return typeList.get(cate);
    }

    /**
     * 按枚举类别获取枚举集合(响应用)
     * @param cate
     * @return
     */
    public static List<Map<String, String>> getItemListByCate(String cate) {
        final List<MyType> myTypes = typeList.get(cate);
        if (CollectionUtils.isEmpty(myTypes)) return Collections.EMPTY_LIST;
        final ArrayList<Map<String, String>> items = new ArrayList<>(myTypes.size());
        for (MyType myType : myTypes) {
            items.add(myType.getItem());
        }
        return items;
    }

    /**
     * 按枚举类别和code获取名称
     * @param cate
     * @param code
     * @return
     */
    public static String getNameBy(String cate, Integer code) {
        String key =  cate + SEPARATOR + code;
        return typeMap.get(key);
    }

    public Map<String, String> getItem() {
        Map<String, String> item = new ConcurrentHashMap<>();
        item.put("instName", name());
        item.put("instIndex", String.valueOf(this.ordinal()));
        item.put("name", this.name);
        item.put("value", String.valueOf(this.value));
        return item;
    }
}

  

但是老板认为一个枚举类不应该枚举所有的状态种类,一个枚举类就只负责一个特定状态类别:

例如:

1、审批状态

2、盖印状态

3、借还状态

 

 

翻译交给另外一个工具类来完成

加上我们已经习惯字典翻译框架的开发方式

在尽可能不动框架的情况下,就通过工具类来完成:

package cn.hyite.amerp.common.util;

import cn.hyite.amerp.common.state.ApproveStateEnum;
import cn.hyite.amerp.common.state.LendStateEnum;
import cn.hyite.amerp.common.state.StampStateEnum;
import cn.hyite.amerp.system.common.dict.dto.DictDTO;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.StringRedisTemplate;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;

/**
 * 枚举缓存工具类,沿用Redis字典缓存方式初始化缓存
 * @author OnCloud9
 * @version 1.0
 * @project amerp-server
 * @date 2022年11月24日 09:42
 */
public class StateEnumUtil {
    private static final String DICT_CACHE_LISTS = "dict_cache_lists";
    private static final String DICT_CACHE_KEYS = "dict_cache_keys";
    private static final String SEPARATOR = "|";
    private static final String DEFAULT_TABLE = "sys_co_dict";
    private static final List<DictDTO> enumList = new ArrayList<>();

    static {
        List<DictDTO> approveStateList = transToDictList(ApproveStateEnum.values(), ApproveStateEnum::getCode, ApproveStateEnum::getName, ApproveStateEnum.CATE);
        List<DictDTO> stampStateList = transToDictList(StampStateEnum.values(), StampStateEnum::getCode, StampStateEnum::getName, StampStateEnum.CATE);
        List<DictDTO> lendStateList = transToDictList(LendStateEnum.values(), LendStateEnum::getCode, LendStateEnum::getName, LendStateEnum.CATE);

        enumList.addAll(approveStateList);
        enumList.addAll(stampStateList);
        enumList.addAll(lendStateList);
    }

    public static void initialize() {
        /* 1、获取RedisTemplate */
        StringRedisTemplate stringRedisTemplate = SpringContext.getBean(StringRedisTemplate.class);
        HashOperations<String, Object, Object> opsForHash = stringRedisTemplate.opsForHash();

        /* 2、准备redis装载容器 */
        Map<String, String> tmpMap = new HashMap<>();
        Map<String, List<DictDTO>> tmpListMap = new HashMap<>();

        /* 3、将枚举按字典的方式拼装 */
        for (DictDTO dto : enumList) {
            String diCateIdent = dto.getDiCateIdent();
            String diCode = dto.getDiCode();

            /* hKey -> 字典表名 | 代码类别 | 代码编号  */
            String key = DEFAULT_TABLE + SEPARATOR + diCateIdent + SEPARATOR + diCode;
            tmpMap.put(key.toUpperCase(), GsonUtil.toJson(dto));

            /* hKey -> 字典表名 | 代码类别 */
            String key2 = DEFAULT_TABLE + SEPARATOR + diCateIdent;
            List<DictDTO> list = tmpListMap.get(key2.toUpperCase());
            if (CollectionUtils.isEmpty(list)) {
                list = new ArrayList<>();
                tmpListMap.put(key2.toUpperCase(), list);
            }
            list.add(dto);
        }

        /* 4、推入Redis中,不做删除操作,共用缓存Key */
        opsForHash.putAll(DICT_CACHE_KEYS, tmpMap);
        opsForHash.putAll(DICT_CACHE_LISTS, toJsonMap(tmpListMap));
        LogUtil.COUNT_LOG.info("加载枚举缓存字典到Redis完毕...");
    }

    private static Map<String, String> toJsonMap(Map<String, ?> tmpListMap) {
        Map<String, String> jsonList = new HashMap<>(tmpListMap.size());
        for (Map.Entry<String, ?> entry : tmpListMap.entrySet()) {
            jsonList.put(entry.getKey(), GsonUtil.toJson(entry.getValue()));
        }
        return jsonList;
    }

    /**
     * 将枚举类集合转换为字典集合
     * @param eArr 枚举类数组
     * @param diCodeFunc 设置diCode取值方法
     * @param diNameFunc 设置diName取值方法
     * @param diCateIdent 设置diCateIdent值
     * @return List<DictDTO>
     * @author OnCloud9
     * @date 2022/11/24 10:40
     */
    private static <StateEnum extends Enum, FieldType> List<DictDTO> transToDictList(StateEnum[] eArr, Function<StateEnum, FieldType> diCodeFunc, Function<StateEnum, FieldType> diNameFunc, String diCateIdent) {
        List<DictDTO> dictList = new ArrayList<>(eArr.length);
        for (StateEnum e : eArr) {
            DictDTO dict = new DictDTO();
            /* 1、设置字段编码 */
            FieldType diCode = diCodeFunc.apply(e);
            dict.setDiCode(String.valueOf(diCode));

            /* 2、设置字段名称 */
            FieldType diName = diNameFunc.apply(e);
            dict.setDiName(String.valueOf(diName));

            /* 3、设置字段类别 */
            dict.setDiCateIdent(diCateIdent);

            /* 4、装载 */
            dictList.add(dict);
        }
        return dictList;
    }
}

  

在处理的钩子方法这里加上工具类的初始化:

package cn.hyite.amerp.common.cache;

import cn.hyite.amerp.common.cache.impl.RedisCacheDictServiceImpl;
import cn.hyite.amerp.common.cache.intf.CacheDictService;
import cn.hyite.amerp.common.util.SpringContext;
import cn.hyite.amerp.common.util.StateEnumUtil;

/**
 * 业务数据缓存管理
 *
 * @version 1.0
 * @project portal-server
 * @author lianss
 * @date 2019年2月25日 下午6:19:36
 */
public abstract class CacheManager {

    /**
     * 刷新缓存
     *
     * @author lianss
     * @date :2019年2月25日 下午6:24:49
     */
    public static void refreshCache() {
        Thread refreshThread = new Thread(new RefreshCache());
        refreshThread.start();
    }
}

class RefreshCache extends Thread {
    @Override
    public void run() {
        CacheDictService cacheDictService = SpringContext.getBean(RedisCacheDictServiceImpl.class);
        cacheDictService.init();
        StateEnumUtil.initialize();
    }
}

  

这里的难题主要是有各种各样的枚举类,需要有一个统一的方法来转换

提供枚举类,和字典属性的取值方法,可以生成字典集合

但是我不会写Lambda方法入参啊。。。,然后参考了一下MybatisPlus的lambdaQuery()源码,写出来了,

    /**
     * 将枚举类集合转换为字典集合
     * @param eArr 枚举类数组
     * @param diCodeFunc 设置diCode取值方法
     * @param diNameFunc 设置diName取值方法
     * @param diCateIdent 设置diCateIdent值
     * @return List<DictDTO>
     * @author OnCloud9
     * @date 2022/11/24 10:40
     */
    private static <StateEnum extends Enum, FieldType> List<DictDTO> transToDictList(StateEnum[] eArr, Function<StateEnum, FieldType> diCodeFunc, Function<StateEnum, FieldType> diNameFunc, String diCateIdent) {
        List<DictDTO> dictList = new ArrayList<>(eArr.length);
        for (StateEnum e : eArr) {
            DictDTO dict = new DictDTO();
            /* 1、设置字段编码 */
            FieldType diCode = diCodeFunc.apply(e);
            dict.setDiCode(String.valueOf(diCode));

            /* 2、设置字段名称 */
            FieldType diName = diNameFunc.apply(e);
            dict.setDiName(String.valueOf(diName));

            /* 3、设置字段类别 */
            dict.setDiCateIdent(diCateIdent);

            /* 4、装载 */
            dictList.add(dict);
        }
        return dictList;
    }

  

 

2022年12月1日 22点22分 更新

解决手动指定字段,改用注解实现:

-- 字典类别标记
package cn.cloud9.server.struct.enums.annotation;

import java.lang.annotation.*;

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

-- 字典编号标记
package cn.cloud9.server.struct.enums.annotation;

import java.lang.annotation.*;

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

-- 字典名称标记
package cn.cloud9.server.struct.enums.annotation;

import java.lang.annotation.*;

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

  

解决手动编码加载的问题:

package cn.cloud9.server.struct.enums.hook;

import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternUtils;
import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.stereotype.Component;
import org.springframework.util.ClassUtils;
import org.springframework.util.SystemPropertyUtils;

import java.util.HashSet;
import java.util.Set;

/**
 * @author OnCloud9
 * @description
 * @project tt-server
 * @date 2022年11月27日 下午 10:43
 */
@Slf4j
@Component
public class ScanSupport implements ResourceLoaderAware {

    private ResourceLoader resourceLoader;
    private final ResourcePatternResolver resolver = ResourcePatternUtils.getResourcePatternResolver(resourceLoader);
    private final MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(resourceLoader);

    @Override
    public void setResourceLoader(ResourceLoader resourceLoader) {
        this.resourceLoader = resourceLoader;
    }

    /**
     * 扫描公共接口包,获取所有的公共接口并加入白名单
     *
     */
    @SneakyThrows
    public Set<Class<?>> doScan(String classPath) {
        Set<Class<?>> classes = new HashSet<>();
        String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX
                .concat(ClassUtils.convertClassNameToResourcePath(SystemPropertyUtils.resolvePlaceholders(classPath))
                .concat("/**/*.class"));
        Resource[] resources = resolver.getResources(packageSearchPath);
        MetadataReader metadataReader = null;
        for (Resource resource : resources) {
            if (!resource.isReadable()) continue;
            metadataReader = metadataReaderFactory.getMetadataReader(resource);
            try {
                classes.add(Class.forName(metadataReader.getClassMetadata().getClassName()));
            } catch (Exception e) {
                log.info("公共接口信息解析错误:{}", e.getMessage());
            }
        }
        return classes;
    }
}

  

按指定包下加载所有类,根据注解标记装填进入Redis:

package cn.cloud9.server.struct.enums;

import cn.cloud9.server.struct.dict.dto.DictDTO;
import cn.cloud9.server.struct.enums.annotation.DictCate;
import cn.cloud9.server.struct.enums.annotation.DictCode;
import cn.cloud9.server.struct.enums.annotation.DictName;
import cn.cloud9.server.struct.enums.hook.ScanSupport;
import cn.cloud9.server.struct.lambda.ObjectBuilder;
import cn.cloud9.server.struct.spring.SpringContextHolder;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

import static cn.cloud9.server.struct.dict.service.DictService.*;

/**
 * @author OnCloud9
 * @description
 * @project tt-server
 * @date 2022年11月27日 下午 08:53
 */
@Slf4j
@SuppressWarnings("all")
public class EnumUtil {
    private static final List<Class> enumClass = new ArrayList<>();
    private static final String CLASS_SCAN_PACKAGE_PATH = "cn.cloud9.server.struct.enums.state";

    @SneakyThrows
    public static void findEnumClassByPath() {
        if (CollectionUtils.isNotEmpty(enumClass)) return;
        try {
            final ScanSupport scanSupport = SpringContextHolder.getBean(ScanSupport.class);
            final Set<Class<?>> classes = scanSupport.doScan(CLASS_SCAN_PACKAGE_PATH);
            enumClass.addAll(classes);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void initialize() {
        findEnumClassByPath();

        RedisTemplate<String, Map<String, String>> mapTemplate = SpringContextHolder.getBean("redisTemplate", RedisTemplate.class);
        final HashOperations<String, Object, Object> hashOps = mapTemplate.opsForHash();

        String sqlKey = "default";
        /* 准备缓存结构容器, 并装载数据 */
        Map<String, String> mapTank = new ConcurrentHashMap<>();
        Map<String, List<DictDTO>> listTank = new ConcurrentHashMap<>();

        final List<DictDTO> dicts = transEnumsToDicts();
        for (DictDTO dict : dicts) {
            final Long dictCode = dict.getDictCode();
            final String dictName = dict.getDictName();
            final String dictType = dict.getDictType();

            /* 装载 key -> h-key -> h-value */
            final String mapKey = sqlKey + SEPARATOR + dictType + SEPARATOR + dictCode;
            mapTank.put(mapKey, dictName);

            /* 装载 key -> h-key -> h-list */
            final String listKey = sqlKey + SEPARATOR + dictType;
            List<DictDTO> cateList = listTank.get(listKey);
            if (CollectionUtils.isEmpty(cateList)) {
                cateList = new ArrayList<>();
                listTank.put(listKey, cateList);
            }
            cateList.add(dict);
        }

        /* 装填到Redis中 */
        hashOps.putAll(KEY_MAP, mapTank);
        hashOps.putAll(KEY_LISTS, listTank);

        log.info("Redis 枚举缓存装载完毕 ...... ");
    }

    /**
     * 将声明的枚举集合转换为字典集合
     * @return
     */
    @SneakyThrows
    private static List<DictDTO> transEnumsToDicts() {
        final List<DictDTO> dicts = new ArrayList<>();
        for (Class enumClass : enumClass) {
            if (!enumClass.isEnum()) continue;
            final String cateName = enumClass.getSimpleName();
            final Object[] enumInstances = enumClass.getEnumConstants();
            for (Object inst : enumInstances) {

                /* 反射,寻找注解的编号字段 */
                final Field codeField = findFieldByAnnotaion(enumClass, DictCode.class);
                final Object codeVal = codeField.get(inst);

                /* 反射,寻找注解的名字字段 */
                final Field nameField = findFieldByAnnotaion(enumClass, DictName.class);
                final Object nameVal = nameField.get(inst);

                /* 反射,寻找注解的类别字段 */
                final Field cateField = findFieldByAnnotaion(enumClass, DictCate.class);
                final Object cateVal = cateField.get(inst);

                DictDTO dto = ObjectBuilder
                        .<DictDTO>builder(DictDTO::new)
                        .with(DictDTO::setDictType, String.valueOf(cateVal))
                        .with(DictDTO::setDictCode, Long.valueOf(String.valueOf(codeVal)))
                        .with(DictDTO::setDictName, String.valueOf(nameVal))
                        .build();
                dicts.add(dto);
            }
        }
        return dicts;
    }

    /**
     * 获取枚举类中标记了指定注解的字段
     * @param targetClass 枚举目标类对象
     * @param annotationClass 声明的注解类对象
     * @param <T> 注解泛型
     * @param <E> 枚举泛型
     * @return 被注解的字段
     */
    @SneakyThrows
    private static <T extends Annotation, E extends Enum> Field findFieldByAnnotaion(Class<E> targetClass, Class<T> annotationClass) {
        final Field targetField = Arrays
                .stream(targetClass.getDeclaredFields())
                .filter(field -> Objects.nonNull(field.getAnnotation(annotationClass)))
                .findFirst()
                .orElse(null);
        if (Objects.isNull(targetField)) throw new RuntimeException(targetClass.getName() + " 没有声明@" + annotationClass.getName() + "注解!!加载失败!!!");
        targetField.setAccessible(true);
        return targetField;
    }
}

  

 

posted @ 2022-11-24 18:45  emdzz  阅读(597)  评论(0编辑  收藏  举报