掘金 后端 ( ) • 2024-06-08 09:53

highlight: agate

平时工作经常需要把字典参数[code] 转成可以理解的中文每次都要一个个的查询太麻烦啦😫! 所以能不能直接缓存起来,直接用;今天就实现一下😎。

🤔问题难点:

  1. 公司的字典参数是保存在表里面的,所以增删改都需要保持一致。
  2. 这个字典释义是给前端展示给用户用的,我后台写接口时不用做转换工作。
  3. 如何代码改动最小...

code.........

开启缓存

使用springboot 自带的缓存;在项目代码中添加 @EnableCaching

@EnableCaching
@SpringBootApplication
public class MainApplication {

    public static void main(String[] args) {
       SpringApplication.run(MainApplication.class, args);
    }

}

创建一个字典缓存类

看了springboot 自带的缓存框架只需要实现Cache就可以啦;由于我这里为了方便,你可以直接继承ConcurrentMapCache就可以啦;为了防止缓存被更改我这里都是拷贝副本。 image.png

/**
 * 字典缓存
 * <br/>
 * date: 2024/5/24<br/>
 * version 0.1
 *
 * @author ls<br />
 */
@Component
public class DicCache implements Cache {

    /**
     * cache name
     */
    public static final String name = "DIC_CACHE";

    private final ConcurrentMap<String, CacheValue> store;

    public DicCache() {
        store = new ConcurrentHashMap<>();
    }

    public void put(String key, EntityDicAO value) {
        if (null == key || null == value) return;
        EntityDicAO newValue = (EntityDicAO) copy(value);
        this.store.put(key, new CacheValue(newValue));
    }

    public CacheValue putIfAbsent(String key, EntityDicAO value) {
        if (null == key || null == value) return null;

        EntityDicAO newValue = (EntityDicAO) copy(value);
        return this.store.putIfAbsent(key, new CacheValue(newValue));
    }

    /**
     * 从缓存中删除
     *
     * @param key index
     */
    public void evict(String key) {
        this.store.remove(key);
    }

    /**
     * 从缓存中删除
     *
     * @param key index
     */
    public void remove(String key) {
        this.evict(key);
    }


    /**
     * Return the cache name.
     */
    @Override
    public String getName() {
        return name;
    }

    /**
     * Return the underlying native cache provider.
     */
    @Override
    public Object getNativeCache() {
        return this.store;
    }

    public EntityDicAO get(String key) {
        CacheValue cacheValue = this.store.get(key);
        return (EntityDicAO) copy(cacheValue.getValue());
    }

    @Override
    public ValueWrapper get(Object key) {
        if (!this.store.containsKey(key)) {
            return null;
        }

        CacheValue cacheValue = this.store.get(key);
        return (ValueWrapper) copy(cacheValue);
    }

    @Override
    public <T> T get(Object key, Class<T> type) {
        return (T) this.get((String) key);
    }

    @Override
    public <T> T get(Object key, Callable<T> valueLoader) {
        CacheValue value = this.store.computeIfAbsent((String) key, r -> {
            try {
                T loaderValue = valueLoader.call();
                if ((loaderValue instanceof EntityDicAO)) {
                    EntityDicAO loaderValueDic = (EntityDicAO) loaderValue;
                    CacheValue cacheValue = new CacheValue((EntityDicAO) this.copy(loaderValueDic));
                    return cacheValue;
                }
                return null;
            } catch (Exception ex) {
                throw new ValueRetrievalException(key, valueLoader, ex);
            }
        });
        return (T) Optional.ofNullable(value).map(CacheValue::getValue).orElse(null);
    }

    @Override
    public void put(Object key, Object value) {
        if (value instanceof EntityDicAO) {
            this.put((String) key, (EntityDicAO) value);
            return;
        }

        if (value instanceof ServiceResult) {
            ServiceResult valueSt = (ServiceResult) value;
            Object data = valueSt.getData();
            if (data instanceof EntityDicAO) {
                this.put((String) key, (EntityDicAO) data);
            }
        }
    }

    @Override
    public ValueWrapper putIfAbsent(Object key, Object value) {
        CacheValue cacheValue = this.putIfAbsent((String) key, (EntityDicAO) value);
        return new SimpleValueWrapper(cacheValue.getValue());
    }

    @Override
    public void evict(Object key) {
        this.evict((String) key);
    }

    @PreDestroy
    public void clear() {
        this.store.clear();
    }

    public EntityDicAO findTypeWithCode(String type, String code) {
        Optional<CacheValue> value = this.store.values().stream().filter(e -> e.getKey().equals(CacheValue.generateKey(type, code))).findFirst();

        return (EntityDicAO) value.map(CacheValue::getValue).map(this::copy).orElse(null);
    }

    /**
     * 缓存包装
     */
    static class CacheValue implements ValueWrapper, Serializable {

        private final String key;

        private final EntityDicAO value;

        CacheValue(EntityDicAO value) {
            this.key = generateKey(value);
            this.value = value;
        }

        public String getKey() {
            return key;
        }

        public EntityDicAO getValue() {
            return value;
        }

        private static String generateKey(EntityDicAO value) {
            // 确保使用用户的唯一标识作为缓存键的一部分
            return generateKey(value.getType(), value.getCode());
        }

        protected static String generateKey(String type, String code) {
            // 确保使用用户的唯一标识作为缓存键的一部分
            return MessageFormat.format("{0}@{1}", type, code);
        }

        /**
         * Return the actual value in the cache.
         */
        @Override
        public Object get() {
            return this.getValue();
        }
    }

    /**
     * 复制字典
     *
     * @param value 字典
     * @return 新的字典
     */
    private <T extends Serializable> Serializable copy(T value) {
        return SerializationUtils.clone(value);
    }


}

添加一个 cache 配置类

DicCache 添加到cacheManager

@Configuration
public class DicCacheConfig {


    @Autowired
    private DicCache dicCache;


    @Bean
    public CacheManager cacheManager() {
        SimpleCacheManager cacheManager = new SimpleCacheManager();
        cacheManager.setCaches(Collections.singletonList(dicCache));
        return cacheManager;
    }

}

字典服务改造

现在只需要在字典服务中以最小的改动进行修改,添加@CachePut、@CacheEvict 注解就可以实现增删改一致啦

public class DicService extends ServiceImpl<EntityDicGeneratedMapper, EntityDicAO> {

/**
 * 保存参数信息
 *
 * @param dicAO 参数信息
 * @return 是否成功
 */
@CachePut(value = DicCache.name,key = "#dicAO.id",condition = "#result.succeed == true ")
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
public ServiceResult<EntityDicAO> createOrUpdateDic(EntityDicAO dicAO) {
    //......业务逻辑代码
}

/**
 * 删除参数信息
 *
 * @param id 参数信息ID
 * @return 是否成功
 */
@CacheEvict(value = DicCache.name,key = "#id",condition = "#result.succeed == true ")
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
public ServiceResult<Boolean> deleteDic(String id) {
   //......业务逻辑代码
}
}

实现字典转译

字典释义是给前端展示用的,那是不是数据序列化的时候再做字典转译就可以啦;springboot 默认使用Jackson来做序列化,实现一个jackson 自定义序列化

实现一个自定义注解

package x.x.x;

import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 字典序列化
 * @author ls
 * 2024/5/27
 * @version 1.0
 */
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@JsonSerialize(using = DictSerializerHandel.class) //这里绑定处理类
public @interface DicSerializer {

    /** 字典类型 */
    String type();
    
    /**
    * 字典释义后的json 字段名
    * 默认是原始字段拼接一个Name
     */
    String name() default "";

    /**
     * 值类型
     */
    Type  valueType() default Type.SINGLE;

    /**
    *多值时的分隔符
     */
    String separator() default ",";

    enum Type{
        /**
        *单值
        */
        SINGLE,
         /**
        *多值
        */
        MULTI_VALUE
    }
}

实现一个基于注解的jackson 自定义序列化,这里需要注意一下不要导错包 com.fasterxml.jackson.databind 这个包下的。

package x.x.x;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JacksonStdImpl;
import com.fasterxml.jackson.databind.ser.ContextualSerializer;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;

import java.io.IOException;
import java.util.Arrays;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;

/**
 * 字典序列化处理
 * @author ls
 * 2024/5/27
 * @version 1.0
 */
@JacksonStdImpl
public class DictSerializerHandel extends StdSerializer<String> implements ContextualSerializer {

    private String type;

    private String name;

    private DicSerializer.Type valueType;

    private String separator;

    public DictSerializerHandel() {
        super(String.class);
    }

    @Override
    public void serialize(String value, JsonGenerator gen, SerializerProvider provider) throws IOException {
        if (Objects.isNull(value)) {
            gen.writeObject(null);
            return;
        }

        String currentName = gen.getOutputContext().getCurrentName();

        if(StringUtils.isEmpty(value) || StringUtils.isEmpty(currentName)){
            gen.writeString("");
            return;
        }

        gen.writeString(value);

        // 通过数据字典类型和value获取name
        DicService dicService = SpringUtils.getBean(DicService.class);

        String translatedValue = "";

        switch (valueType){
            case SINGLE:
                EntityDicAO dic = dicService.getDicByTypeWithCode(type, value).getData();
                translatedValue= Optional.ofNullable(dic).map(EntityDicAO::getName).orElse("");
                break;
            case MULTI_VALUE:
                translatedValue = Arrays.stream(value.split(separator)).map(e->dicService.getDicByTypeWithCode(type, e).getData()).filter(Objects::nonNull).map(EntityDicAO::getName).filter(StringUtils::isNotBlank)
                        .distinct().collect(Collectors.joining(separator));
                break;
            default:
        }

        gen.writeStringField(
                StringUtils.isBlank(name) ? currentName.concat("Name") : name,
                translatedValue
        );
    }

    @Override
    public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty beanProperty) {
        if (beanProperty != null && beanProperty.getType().getRawClass().equals(String.class)) {
            // 获取注解的参数
            DicSerializer annotation = beanProperty.getAnnotation(DicSerializer.class);
            if (null != annotation) {
                type = annotation.type();
                name = annotation.name();
                valueType = annotation.valueType();
                separator = annotation.separator();
                return this;
            }
        }
        return new ToStringSerializer();
    }
}

到此就大致可以1啦..................

使用

public class EntityEvaluation  {
    
    // ............
    
    /**
     * 评估类型
     */
    @DicSerializer(type = "assessment")
    private String type;

    /**
     * 评估分析-长期目标 多选 多个用‘,‘号分割
     */
    @DicSerializer(type = "PATIENT_CONCERNS",valueType = DicSerializer.Type.MULTI_VALUE)
    private String patientConcerns;

    // ............
}

测试

image.png

到此完美 增加摸鱼的时间🤣🤣😂

如果你是分布式服务的话推荐 Easy-Trans

image.png