实现数据字典的缓存、加载、刷新和映射的集成框架

3,370 阅读11分钟

前言

在业务开发的过程中,总是会遇到字典打交道。比如说:性别,类型,状态等字段,都可以归纳为字典的范围。

字典的组成分成:字典类型字典数据

其中 字典数据 归属于一类的 字典类型。可以通过 字典类型 获取 字典数据。例如开头提到的,性别就为字典类型,1-男;0-女;就为字典数据。其中“1”和“0”为代码值,“男”和“女”为显示值。数据库中存放代码值,页面上展示显示值。

字典的用途:

  1. 前端下拉框
  2. 列表展示时,需要将代码值转换成显示值
  3. 逻辑条件判断(前后端都需要)
  4. 数据范围限制

在这里,我暂时将字典的来源归纳为三类:枚举字典表业务数据

  • 枚举:使用枚举,可以方便的在代码中做逻辑判断与数据范围限制。
  • 字典表:使用一张或多张字典表,统一管理项目中的字典数据。(可能会与枚举重复)
  • 业务数据:在某些场景下,我们需要将业务数据拿来当字典使用。(数据源,数据表等)

正文

根据前言中的用途,字典框架需要的功能有如下几个:

  1. 缓存字典类型和字典数据
  2. 方便加载实时业务字典数据
  3. 能够根据字典类型和字典数据代码值,进行映射成显示值

所以字典框架也是根据以上三点进行设计:

  1. 缓存(应用启动缓存)
  2. 加载(应用启动加载)
  3. 重新加载(动态字典变更,修改缓存数据)
  4. 序列化(映射)

工程结构图

image.png

一、加载与缓存

为了快速的查找字典,或者用来做映射。最简单的方式,就是缓存数据。将数据缓存到内存中,或者其他中间件中,提高使用效率。

设计

image.png

缓存数据对外操作统一放在 DictDataCache 类中。

在我的设计中,因为字典的数据来源有三个地方,所以缓存也放在了三个不同的位置。使用的是Hutool工具中的缓存工具,并且自己实现了一个自定义缓存类 NoClearCache

自定义永久缓存 NoClearCache

package com.cah.project.core.dict.cache;
import cn.hutool.cache.impl.AbstractCache;
import java.util.HashMap;
/**
 * 功能描述: 无清理缓存 <br/>
 */
public class NoClearCache<K, V> extends AbstractCache<K, V> {
    private static final long serialVersionUID = 1L;
    public NoClearCache() {
        cacheMap = new HashMap<>();
    }
    @Override
    protected int pruneCache() {
        return 0;
    }
}

由三个handler存放。并且使用 枚举工厂 来进行管理 DictLoadEnum。 有兴趣的可以自己查看本人的“策略枚举”专栏。

字典加载枚举 DictLoadEnum

package com.cah.project.core.dict.enums;
import cn.hutool.extra.spring.SpringUtil;
import com.cah.project.core.dict.annotation.DictType;
import com.cah.project.core.dict.handler.BusinessDictHandler;
import com.cah.project.core.dict.handler.DictDataHandler;
import com.cah.project.core.dict.handler.DictEnumHandler;
import com.cah.project.core.dict.handler.IDictHandler;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.stream.Stream;
/**
 * 功能描述: 字典加载枚举 <br/>
 */
@Getter
@AllArgsConstructor
@DictType(typeCode= "LOAD_TYPE", typeName = "字典加载类型")
public enum DictLoadEnum implements IDictEnum {
    /** 枚举(永久缓存) */
    ENUM("1", "枚举", DictEnumHandler.class),
    /** 字典表(先进先出)[如果与枚举冲突,将会覆盖枚举] */
    DICT_TABLE("2", "字典表", DictDataHandler.class),
    /** 业务数据(使用频率) */
    BUSINESS_DATA("3", "业务数据", BusinessDictHandler.class),
    ;
    private final String type;
    private final String msg;
    private final Class<? extends IDictHandler> handler;

    public static DictLoadEnum indexOf(String type) {
        return Stream.of(values()).filter(d -> d.getType().equals(type)).findFirst().orElse(ENUM);
    }

    public IDictHandler getDictHandler() {
        return SpringUtil.getBean(getHandler());
    }

    @Override
    public String getCode() {
        return getType();
    }

    /**
     * 功能描述: 启动加载 <br/>
     */
    public static void start() {
        for(DictLoadEnum dle : DictLoadEnum.values()) {
            dle.getDictHandler().loadDictCache();
        }
    }

}

三个handler实现了统一的接口 IDictHandler,并且继承了抽象类 AbstractDictHandler,在 AbstractDictHandler 中,定义了缓存属性。

protected Cache<String, List<DictDataOptions>> cacheData;

每个实现,自己创建不同的缓存对象,即可实现不同的缓存策略。 如:

  • 枚举缓存,本身就是代码,所以可以设置永久缓存 NoClearCache,不设置大小;
  • 字典表缓存,可以使用 “先进先出缓存” FIFOCache,并且设置大小为10000个;
  • 业务数据缓存,可以使用 “最少使用缓存” LFUCache,并且设置大小为10000个;

接口 IDictHandler

loadDictCache() 方法进行加载缓存。

reloadDictCache() 重新加载缓存。须配合 Event 事件使用

getDictDataOptions() 获取字典数据。

package com.cah.project.core.dict.handler;
import com.cah.project.core.dict.domain.vo.DictDataOptions;
import com.cah.project.core.dict.event.DictEvent;
import org.springframework.context.ApplicationListener;
import java.util.List;
/***
 * 功能描述: 字典加载接口 <br/>
 */
public interface IDictHandler extends ApplicationListener<DictEvent> {
    /**
     * 功能描述: 加载缓存 <br/>
     */
    void loadDictCache();
    /**
     * 功能描述: 重新加载缓存 <br/>
     */
    void reloadDictCache(String typeCode);
    /**
     * 功能描述: 通过字典类型,获取字典数据 <br/>
     *
     * @param typeCode 字典类型
     * @return "java.util.List<com.cah.project.module.standard.domain.vo.out.DictDataOptions>"
     */
    List<DictDataOptions> getDictDataOptions(String typeCode);
}

抽象类 AbstractDictHandler

package com.cah.project.core.dict.handler;
import cn.hutool.cache.Cache;
import cn.hutool.core.collection.ListUtil;
import com.cah.project.core.dict.cache.DictDataCache;
import com.cah.project.core.dict.domain.vo.DictDataOptions;
import com.cah.project.core.dict.event.DictEvent;
import java.util.List;
/**
 * 功能描述: 字典处理,抽象类 <br/>
 */
public abstract class AbstractDictHandler implements IDictHandler {
    /***
     * 功能描述: 缓存对象 <br/>
     */
    protected Cache<String, List<DictDataOptions>> cacheData;
    public AbstractDictHandler() {}
    @Override
    public void onApplicationEvent(DictEvent event) {
        reloadDictCache(event.getTypeCode());
    }
    /**
     * 功能描述: 添加字典类型 <br/>
     *
     * @param typeCode 字典类型
     * @param typeName 字典类型名称
     */
    protected void addDictType(String typeCode, String typeName) {
        DictDataCache.addType(typeCode, typeName);
    }
    /***
     * 功能描述: 批量添加字典数据缓存 <br/>
     *
     * @param typeCode 字典类型
     * @param dataList 字典数据列表
     */
    protected void put(String typeCode, List<DictDataOptions> dataList) {
        if(cacheData.get(typeCode) == null) {
            cacheData.put(typeCode, ListUtil.list(false));
        }
        cacheData.get(typeCode).addAll(dataList);
    }
    /**
     * 功能描述: 单条添加字典数据缓存 <br/>
     *
     * @param typeCode 字典类型
     * @param dataValue 字典值
     * @param dataLabel 字典标签
     */
    protected void put(String typeCode, String dataValue, String dataLabel) {
        if(cacheData.get(typeCode) == null) {
            cacheData.put(typeCode, ListUtil.list(false));
        }
        cacheData.get(typeCode).add(DictDataOptions.builder().typeCode(typeCode).dataValue(dataValue).dataLabel(dataLabel).build());
    }
    /**
     * 功能描述: 移除缓存 <br/>
     *
     * @param typeCode 字典类型
     */
    protected void remove(String typeCode) {
        cacheData.remove(typeCode);
    }
}

1、枚举

在代码中,如何才能自动并且便捷的将枚举加入到缓存中呢?我定义了一个注解 DictType ,还有一个接口 IDictEnum。通过这两个类,就能够使用反射,将枚举类加入到枚举缓存中。具体的使用可以参考上面的 DictLoadEnum 类。

枚举字典注解 DictType

package com.cah.project.core.dict.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
 * 功能描述: 字典类型注解 <br/>
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface DictType {
    /** 字典类型 */
    String typeCode();
    /** 字典名称 */
    String typeName();
}

枚举字典接口 IDictEnum

package com.cah.project.core.dict.enums;
/**
 * 功能描述: 字典枚举接口 <br/>
 */
public interface IDictEnum {
    /**
     * 功能描述: 代码值 <br/>
     */
    String getCode();
    /**
     * 功能描述: 显示值 <br/>
     */
    String getMsg();
}

实现方式如下:通过包和接口扫描,循环判断是否符合要求。在通过条件,获取数据,加入到缓存中。

枚举字典处理者 DictEnumHandler

package com.cah.project.core.dict.handler;
import cn.hutool.core.util.ClassUtil;
import com.cah.project.core.dict.annotation.DictType;
import com.cah.project.core.dict.cache.NoClearCache;
import com.cah.project.core.dict.domain.vo.DictDataOptions;
import com.cah.project.core.dict.enums.IDictEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Set;
@Slf4j
@Component
public class DictEnumHandler extends AbstractDictHandler {
    // 通过配置,实现包的扫描
    @Value("${dict.enum.package:com.cah.project}")
    private String packageStr;

    public DictEnumHandler() {
        cacheData = new NoClearCache<>();
    }
    @Override
    public List<DictDataOptions> getDictDataOptions(String typeCode) {
        return cacheData.get(typeCode);
    }
    @Override
    public void loadDictCache() {
        try {
            // 如果没有数据,进行注册
            Set<Class<?>> classes = ClassUtil.scanPackageBySuper(packageStr, IDictEnum.class);
            for (Class<?> aClass : classes) {
                if (ClassUtil.isEnum(aClass)) {
                    DictType dictType = aClass.getAnnotation(DictType.class);
                    if (dictType == null) {
                        throw new RuntimeException("枚举:" + aClass.getName() + " 没有添加@DictType注解。");
                    }
                    Method codeMethod = ClassUtil.getDeclaredMethod(aClass, "getCode");
                    Method msgMethod = ClassUtil.getDeclaredMethod(aClass, "getMsg");
                    if (codeMethod == null || msgMethod == null) {
                        continue;
                    }
                    //得到enum的所有实例
                    Object[] objList = aClass.getEnumConstants();
                    for (Object obj : objList) {
                        put(dictType.typeCode(), (String) codeMethod.invoke(obj), (String) msgMethod.invoke(obj));
                    }
                    // 添加字典类型
                    addDictType(dictType.typeCode(), dictType.typeName());
                }
            }
        } catch (IllegalAccessException | InvocationTargetException e) {
            log.error("枚举缓存加载异常:{}", e.getMessage());
        }
    }
    @Override
    public void reloadDictCache(String typeCode) {
        // 由于的内存的,不需要重新加载
    }
}

2、字典表

字典表的加载需要下发,所以重新定义了字典表处理接口 IDictDataHandler, 并且使用抽象类 AbstractDictDataHandler 实现了通用方法,以及默认实现类 DefaultDictDataHandler。这样就能保证框架的完整性,不强依赖业务。

字典表处理者 DictDataHandler

package com.cah.project.core.dict.handler;
import cn.hutool.cache.CacheUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.ListUtil;
import com.cah.project.core.dict.cache.DictDataCache;
import com.cah.project.core.dict.domain.vo.DictDataOptions;
import com.cah.project.core.dict.handler.dictData.IDictDataHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.List;
/**
 * 功能描述: 字典表加载 <br/>
 */
@Component
public class DictDataHandler extends AbstractDictHandler {
    @Autowired
    private DictHandlerAware dictHandlerAware;

    public DictDataHandler() {
        cacheData = CacheUtil.newFIFOCache(10000);
    }
    @Override
    public void loadDictCache() {
        // 加载字典缓存
        getDictDataHandler().loadDictCache(DictDataCache.getTypeList(), cacheData);
    }
    @Override
    public void reloadDictCache(String typeCode) {
        remove(typeCode);
        List<DictDataOptions> dictDataList = getDictDataHandler().getDictDataOptions(typeCode);
        if(CollUtil.isNotEmpty(dictDataList)) {
            put(typeCode, dictDataList);
        }
    }
    @Override
    public List<DictDataOptions> getDictDataOptions(String typeCode) {
        List<DictDataOptions> dictDataOptions = cacheData.get(typeCode);
        // 如果缓存中不存在,则说明可能失效了,再次从数据库中查询
        if(CollUtil.isEmpty(dictDataOptions)) {
            List<DictDataOptions> dictDataList = getDictDataHandler().getDictDataOptions(typeCode);
            if(CollUtil.isEmpty(dictDataList)) {
                put(typeCode, dictDataList);
            } else {
                return ListUtil.empty();
            }
        }
        return cacheData.get(typeCode);
    }
    
    private IDictDataHandler getDictDataHandler() {
        return dictHandlerAware.getDictHandler();
    }
}

字典表接口 IDictDataHandler

package com.cah.project.core.dict.handler.dictData;
import cn.hutool.cache.Cache;
import com.cah.project.core.dict.domain.vo.DictDataOptions;
import com.cah.project.core.dict.domain.vo.DictTypeOptions;
import java.util.Collection;
import java.util.List;
import java.util.Set;

public interface IDictDataHandler {

    /**
     * 功能描述: 获取全部的字典类型 <br/>
     *
     * @return "java.util.List<com.cah.project.module.standard.domain.entity.DictTypeEntity>"
     */
    Set<DictTypeOptions> getDictTypeList();

    /**
     * 功能描述: 通过字典类型,获取字典数据 <br/>
     *
     * @param typeCode 字典类型
     * @return "java.util.List<com.cah.project.module.standard.domain.vo.out.DictDataOptions>"
     */
    List<DictDataOptions> getDictDataOptions(String typeCode);

    /**
     * 功能描述: 通过批量字典类型,查询字典数据 <br/>
     *
     * @param typeCodeList 字典类型集合
     * @return "java.util.List<com.cah.project.module.standard.domain.vo.out.DictDataOptions>"
     */
    List<DictDataOptions> getDictDataOptions(Collection<String> typeCodeList);

    /**
     * 功能描述: 加载缓存 <br/>
     *
     * @param dictTypeList 字典类型列表
     * @param cacheData 字典缓存数据
     */
    void loadDictCache(Collection<DictTypeOptions> dictTypeList, Cache<String, List<DictDataOptions>> cacheData);

}

字典表抽象实现 AbstractDictDataHandler

package com.cah.project.core.dict.handler.dictData;
import cn.hutool.cache.Cache;
import cn.hutool.core.collection.ListUtil;
import com.cah.project.core.dict.domain.vo.DictDataOptions;
import com.cah.project.core.dict.domain.vo.DictTypeOptions;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/**
 * 功能描述: 字典表加载 <br/>
 */
public abstract class AbstractDictDataHandler implements IDictDataHandler {
    /** 定义分页每页大小 */
    protected static final int PAGE_SIZE = 50;
    @Override
    public void loadDictCache(Collection<DictTypeOptions> dictTypeList, Cache<String, List<DictDataOptions>> cacheData) {
        Set<DictTypeOptions> dictTypeOptionsSet = getDictTypeList();
        dictTypeList.addAll(dictTypeOptionsSet);
        List<String> typeCodeList = dictTypeOptionsSet.stream().map(DictTypeOptions::getTypeCode).collect(Collectors.toList());
        int total = typeCodeList.size() / PAGE_SIZE + 1;
        // 分页加载字典数据
        for(int i = 0; i < total; i++) {
            List<DictDataOptions> dictDataList = getDictDataOptions(ListUtil.page(i, PAGE_SIZE, typeCodeList));
            for(DictDataOptions ddp : dictDataList) {
                if(cacheData.get(ddp.getTypeCode()) == null) {
                    cacheData.put(ddp.getTypeCode(), ListUtil.list(false));
                }
                cacheData.get(ddp.getTypeCode()).add(ddp);
            }
        }
    }
}

字典表默认实现 DefaultDictDataHandler

package com.cah.project.core.dict.handler.dictData;
import com.cah.project.core.dict.domain.vo.DictDataOptions;
import com.cah.project.core.dict.domain.vo.DictTypeOptions;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
/**
 * 功能描述: 默认实现 <br/>
 */
public class DefaultDictDataHandler extends AbstractDictDataHandler {
    public DefaultDictDataHandler() {
        super();
    }
    @Override
    public Set<DictTypeOptions> getDictTypeList() {
        return Collections.emptySet();
    }
    @Override
    public List<DictDataOptions> getDictDataOptions(String typeCode) {
        return Collections.emptyList();
    }
    @Override
    public List<DictDataOptions> getDictDataOptions(Collection<String> typeCodeList) {
        return Collections.emptyList();
    }
}

具体的业务实现 StandardAbstractDictDataHandler

该实现放在具体的业务服务中,这里作为例子展示。

package com.cah.project.module.standard.service.handler;
import com.cah.project.core.dict.domain.vo.DictDataOptions;
import com.cah.project.core.dict.domain.vo.DictTypeOptions;
import com.cah.project.core.dict.handler.dictData.AbstractDictDataHandler;
import com.cah.project.module.standard.service.IDictDataService;
import com.cah.project.module.standard.service.IDictTypeService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@Slf4j
@Component
public class StandardAbstractDictDataHandler extends AbstractDictDataHandler {
    @Autowired
    private IDictTypeService dictTypeService;
    @Autowired
    private IDictDataService dictDataService;
    @Override
    public Set<DictTypeOptions> getDictTypeList() {
        return dictTypeService.selectAll().stream()
                .map(e -> DictTypeOptions.builder().typeCode(e.getTypeCode()).typeName(e.getTypeName()).build())
                .collect(Collectors.toSet());
    }
    @Override
    public List<DictDataOptions> getDictDataOptions(String typeCode) {
        return dictDataService.selectListByCode(typeCode).stream()
                .map(e -> DictDataOptions.builder().typeCode(e.getTypeCode()).dataLabel(e.getDataLabel()).dataValue(e.getDataValue()).build())
                .collect(Collectors.toList());
    }
    @Override
    public List<DictDataOptions> getDictDataOptions(Collection<String> typeCodeList) {
        return dictDataService.selectListByCodes(typeCodeList).stream()
                .map(e -> DictDataOptions.builder().typeCode(e.getTypeCode()).dataLabel(e.getDataLabel()).dataValue(e.getDataValue()).build())
                .collect(Collectors.toList());
    }
}

3、业务数据

业务数据类型的字典,与字典表类似。但又不完全一致。基本上一种业务表,就是一个字典类型和字典数据。所以设计的方式与字典表不相同。

业务字典处理接口 IBusinessDictHandler

typeCode() 设置业务字典类型

typeName() 设置业务字典类型描述(最好有)

getDictDataList() 获取业务字典数据

package com.cah.project.core.dict.handler.business;
import com.cah.project.core.dict.domain.vo.DictDataOptions;
import java.util.List;
/**
 * 功能描述: 业务字典处理接口 <br/>
 */
public interface IBusinessDictHandler {
    /**
     * 功能描述: 获取字典类型 <br/>
     */
    String typeCode();
    /**
     * 功能描述: 字典描述 <br/>
     */
    String typeName();
    /**
     * 功能描述: 获取字典数据 <br/>
     */
    List<DictDataOptions> getDictDataList();
}

业务字典处理抽象实现 AbstractBusinessDictHandler

没啥好写的,放空就好

package com.cah.project.core.dict.handler.business;
/**
 * 功能描述: 业务字典处理,抽象实现 <br/>
 */
public abstract class AbstractBusinessDictHandler implements IBusinessDictHandler {
}

具体的业务实现 DataSourceDictHandler

package com.cah.project.module.meta.service.handler;
import com.cah.project.core.dict.domain.vo.DictDataOptions;
import com.cah.project.core.dict.handler.business.AbstractBusinessDictHandler;
import com.cah.project.module.meta.domain.entity.DataSourceEntity;
import com.cah.project.module.meta.service.IDataSourceService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.stream.Collectors;
/**
 * 功能描述: 数据源字典处理 <br/>
 */
@Component
public class DataSourceDictHandler extends AbstractBusinessDictHandler {
    @Autowired
    private IDataSourceService dataSourceService;
    @Override
    public String typeCode() {
        return "DATA_SOURCE";
    }
    @Override
    public String typeName() {
        return "数据源";
    }
    @Override
    public List<DictDataOptions> getDictDataList() {
        List<DataSourceEntity> dataSourceList = dataSourceService.selectAll();
        return dataSourceList.stream()
                .map(e -> DictDataOptions.builder().typeCode(typeCode()).dataLabel(e.getName()).dataValue(e.getId()).build())
                .collect(Collectors.toList());
    }
}

二、扫描与启动

1、实现类扫描

字典表也业务数据字典,一般都是属于各个不同的业务系统的。所以在该工程中,只会由一个默认的空实现。只要由具体的实现,即可替换默认实现。该功能由 DictHandlerAware 实现 ApplicationContextAware 接口来达到该目的。

扫描 DictHandlerAware

package com.cah.project.core.dict.handler;
import cn.hutool.core.map.MapUtil;
import com.cah.project.core.dict.handler.business.IBusinessDictHandler;
import com.cah.project.core.dict.handler.dictData.AbstractDictDataHandler;
import com.cah.project.core.dict.handler.dictData.DefaultDictDataHandler;
import com.cah.project.core.dict.handler.dictData.IDictDataHandler;
import lombok.Getter;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
 * 功能描述: 通过spring,将业务字典实现类注册到map中,方便使用 <br/>
 */
@Getter
@Component
public class DictHandlerAware implements ApplicationContextAware {
    // 业务数据字典实现集合(key: 业务字典类型,value:业务字典实现)
    private final Map<String, IBusinessDictHandler> handlerMap = new HashMap<>();
    // 设置默认实现
    private IDictDataHandler dictHandler = new DefaultDictDataHandler();
    /**
     * 功能描述: 获取应用上下文并获取相应的接口实现类 <br/>
     *
     * @param applicationContext 上下文
     */
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        //根据接口类型返回相应的所有bean
        Map<String, IBusinessDictHandler> map = applicationContext.getBeansOfType(IBusinessDictHandler.class);
        Set<Map.Entry<String, IBusinessDictHandler>> entries = map.entrySet();
        for(Map.Entry<String, IBusinessDictHandler> entry : entries) {
            this.handlerMap.put(entry.getValue().typeCode(), entry.getValue());
        }
        // 根据抽象类,返回字典实现
        Map<String, AbstractDictDataHandler> dictMap = applicationContext.getBeansOfType(AbstractDictDataHandler.class);
        if(MapUtil.isNotEmpty(dictMap)) {
            dictHandler = dictMap.entrySet().iterator().next().getValue();
        }
    }
}

2、启动加载

通过实现 ApplicationRunner 接口,实现启动加载字典缓存功能。

启动类 DictStartRunner

package com.cah.project.core.dict;
import com.cah.project.core.dict.enums.DictLoadEnum;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
/**
 * 功能描述: 字典启动加载加载 <br/>
 */
@Component
@Order(value = 2)
public class DictStartRunner implements ApplicationRunner {
    @Override
    public void run(ApplicationArguments args) {
        try {
            DictLoadEnum.start();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

三、刷新

只要是数据库中的数据,就存在变更的情况(新增、修改、删除)。这时,需要修改对应的缓存。

这里使用了 ApplicationEvent 来进行处理。通过自定义 DictEvent 传递 字典类型 typeCode。然后由 IDictHandler 继承 ApplicationListener<DictEvent> 监听接口。抽象处理者 AbstractDictHandler 默认实现 onApplicationEvent(DictEvent event) 方法。并且重新定义抽象方法 abstract void reloadDictCache(String typeCode) 由各个具体实现类编写逻辑。

自定义事件 DictEvent

package com.cah.project.core.dict.event;

import lombok.Getter;
import org.springframework.context.ApplicationEvent;

/**
 * 功能描述: 字典重新加载事件 <br/>
 */
@Getter
public class DictEvent extends ApplicationEvent {

    private final String typeCode;

    /**
     * Create a new {@code ApplicationEvent}.
     *
     * @param source the object on which the event initially occurred or with
     *               which the event is associated (never {@code null})
     */
    public DictEvent(Object source, String typeCode) {
        super(source);
        this.typeCode = typeCode;
    }
}

发送事件类 DictReloadPublishEvent

package com.cah.project.core.dict.event;

import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;

/**
 * 功能描述: 字典重新加载事件操作 <br/>
 */
@Component
public class DictReloadPublishEvent {

    @Resource
    private ApplicationContext applicationContext;

    public void publishEvent(String typeCode) {
        DictEvent de = new DictEvent(this, typeCode);
        applicationContext.publishEvent(de);
    }

}

刷新事件使用

@Autowired
private DictReloadPublishEvent dictReloadPublishEvent;

public void test() {
    dictReloadPublishEvent.publishEvent("SEX_TYPE");
}

四、映射(序列化)

该内容请参考 [Jackson 序列化字典字段属性] 和 [Jackson 序列化字典字段属性(升级)] 的内容。其中的类进行了一次调整。移动到了本工程中。不影响使用。

五、测试

1、字典 Controller

package com.cah.project.module.standard.controller;

import com.cah.project.core.dict.domain.vo.DictDataOptions;
import com.cah.project.core.dict.domain.vo.DictTypeOptions;
import com.cah.project.core.domain.out.CommonResult;
import com.cah.project.module.standard.service.IDictDataService;
import com.cah.project.module.standard.service.IDictTypeService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Collection;
import java.util.List;

/**
 * 功能描述: 字典 <br/>
 */
@RestController
@RequestMapping("/dict")
@Api(tags = {"字典"})
public class DictController {

    @Autowired
    private IDictTypeService dictTypeService;

    @Autowired
    private IDictDataService dictDataService;

    @GetMapping("type/options")
    @ApiOperation(value = "获取字典类型下拉选项")
    public CommonResult<Collection<DictTypeOptions>> selectOptions() {
        return CommonResult.data(dictTypeService.selectOptions());
    }

    @GetMapping("data/options/{typeCode}")
    @ApiOperation(value = "获取字典数据下拉选项")
    public CommonResult<List<DictDataOptions>> selectDataOptions(@PathVariable String typeCode) {
        return CommonResult.data(dictDataService.selectDataOptionsByCode(typeCode));
    }

    @GetMapping("data/options/{loadType}/{typeCode}")
    @ApiOperation(value = "获取字典数据下拉选项,可以根据加载参数,找找不同的数据")
    public CommonResult<List<DictDataOptions>> selectDataOptionsByLoadType(@PathVariable String loadType, @PathVariable String typeCode) {
        return CommonResult.data(dictDataService.selectDataOptionsByLoadType(loadType, typeCode));
    }

}

2、字典Service

package com.cah.project.module.standard.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.cah.project.core.enums.YesOrNoEnum;
import com.cah.project.module.standard.domain.entity.DictTypeEntity;
import com.cah.project.core.dict.domain.vo.DictTypeOptions;
import com.cah.project.module.standard.mapper.DictTypeMapper;
import com.cah.project.module.standard.service.IDictTypeService;
import com.cah.project.core.dict.cache.DictDataCache;
import org.springframework.stereotype.Service;

import java.util.Collection;
import java.util.List;

/**
 * 功能描述: 字典类型 服务实现 <br/>
 */
@Service
public class DictTypeServiceImpl extends ServiceImpl<DictTypeMapper, DictTypeEntity> implements IDictTypeService {

    @Override
    public Collection<DictTypeOptions> selectOptions() {
        // 从缓存中获取
        return DictDataCache.getTypeList();
    }

    @Override
    public List<DictTypeEntity> selectAll() {
        return lambdaQuery().eq(DictTypeEntity::getEnable, YesOrNoEnum.YES.getCode()).list();
    }

}
package com.cah.project.module.standard.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.cah.project.core.dict.cache.DictDataCache;
import com.cah.project.core.dict.domain.vo.DictDataOptions;
import com.cah.project.core.enums.YesOrNoEnum;
import com.cah.project.module.standard.domain.entity.DictDataEntity;
import com.cah.project.module.standard.mapper.DictDataMapper;
import com.cah.project.module.standard.service.IDictDataService;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;

import java.util.Collection;
import java.util.List;

/**
 * 功能描述: 字典数据 服务实现 <br/>
 */
@Service
@RequiredArgsConstructor(onConstructor_ = {@Autowired, @Lazy})
public class DictDataServiceImpl extends ServiceImpl<DictDataMapper, DictDataEntity> implements IDictDataService {

    @Override
    public List<DictDataOptions> selectDataOptionsByCode(String dictType) {
        return DictDataCache.get(dictType);
    }

    @Override
    public List<DictDataOptions> selectDataOptionsByLoadType(String loadType, String dictType) {
        return DictDataCache.get(loadType, dictType);
    }

    @Override
    public List<DictDataEntity> selectListByCode(String dictType) {
        return lambdaQuery().eq(DictDataEntity::getTypeCode, dictType)
                .eq(DictDataEntity::getEnable, YesOrNoEnum.YES.getCode()).list();
    }

    @Override
    public List<DictDataEntity> selectListByCodes(Collection<String> dictTypeList) {
        return lambdaQuery().in(DictDataEntity::getTypeCode, dictTypeList)
                .eq(DictDataEntity::getEnable, YesOrNoEnum.YES.getCode()).list();
    }

}

3、接口测试

获取字典类型列表

image.png

获取字典数据列表-枚举

image.png

获取字典数据列表-字典表

image.png

获取字典数据列表-业务数据

image.png

六、代码地址

[项目工具代码]

总结

描述的不好,会多加改进。欢迎讨论。 后续优化内容:多层级(如省市区,国家地理,行业大类中类小类等)字典如何实现与缓存?是通过拆分成单层级,还是添加多层级内容有待考虑。