Spring缓存组装数据实现

问题描述

数据库的表中存放着code,而页面需要展现这些code对应的中文或英文名称。
如果通过数据库实现,则sql需要类似的写法:

select main_table.* , dict.name, dict2.name
from main_table
left join dict  on main_table.property = dict.id 
left join dict2 on main_table.property2 = dict2.id
...

java端则不需要做任何事情,直接将数据转发给页面展示就可以了。
但问题也很明显,就是查询效率问题。
我见过有很多人直接将所有的信息存到一个表里,或有着方面的打算。当表的数据很大,或者dict的数据有变化,或者表的结构有变更,那将会是一件特别痛苦的事情。

java数据填充的方案

整体流程:

1 完成后处理
2 加载
3 请求
4 获取数据
5 返回数据
6 获取配置信息
7 从缓存获取字典数据
8 填充字典数据
9 返回数据
SpringContainer初始化
自定义BeanFactory后处理器
缓存注解的类
前端请求
切面
service请求数据
Redis

暴露的接口:

只需要在返回数据类型上添加注解就可以。

涉及的技术点:

  • Spring容器声明周期
  • 注解
  • Spring core的公共工具
  • aop
  • redisson
  • mybatis

实现

类注解

作用:让spring能够加载到此类。能够通过spring的applicationcontext获取哪些类配置了注解。

@Target(ElementType.TYPE) //用于class
@Retention(RetentionPolicy.RUNTIME)//运行时可见
@Documented //体现于java doc
@Component //继承Spring的Component注解
public @interface DictClassAnno{}
@Target(ElementType.FIELD) //用于class
@Retention(RetentionPolicy.RUNTIME)//运行时可见
@Documented //体现于java doc
public @interface DictFieldAnno {

    String field();  //字段属性,帮助查找字段对应的值
    
    String category(); //字段属性,帮助查找字段对应的值
}
  • DictClassAnno注解于类上,表明如果返回结果是这个类型,则会对结果进行属性的封装。
  • DictFieldAnno注解于属性上,所在的类需要注解DictClassAnno。表名此属性需要从缓存查找值。field指向本类中的另一个id属性,之后将会通过此id属性的值查找当前属性的值。category分类。这里认为dict的数据是有分组的。

类信息缓存结构

定义一个assembler类,其中的静态属性beanFieldsCache缓存类信息。
其中的静态方法,提供字典数据组装的功能。

package org.yunzhong.image;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

public class DictAssembler {
    private static Map<String, DictBeanInfo> beanFieldsCache = new HashMap<>();
    
    public static void add(String className, DictBeanInfo beanInfo) {
        beanFieldsCache.put(className, beanInfo);
    }
    
    private static void assemble(Object target, DictCacheServiceImpl cacheService) {
        String className = target.getClass().getSimpleName();
        if (!beanFieldsCache.containsKey(className)) {
            return;
        }
        DictBeanInfo dictBeanFieldInfo = beanFieldsCache.get(className);
        Map<String, DictBeanFieldInfo> fields = dictBeanFieldInfo.getFieldInfos();
        for(Interator<Entry<String, DictBeanFieldInfo>> iterator = fields.entrySet().iterator(); iterator.hasNext();) {
            Entry<String, DictBeanFieldInfo> entry = iterator.next();
            DictBeanFieldInfo fieldInfo = entry.getValue();
            Object value = cacheService.get(fieldInfo.getCategory(), RelectionUtils.getField(fieldInof.getIdField(), target));
            ReflectionUtils.setField(fieldInof.getField(), target, value);
        }
    }
    
    public static <T extends Collection<R>, R> T assembleCollection(T targets) {
        if ( CollectionUtils.isEmpty(targets)) {
            return targets;
        }
        DictCacheServiceImpl cacheService = ApplicationContextHolder.getBean(DictCacheServiceImpl.class);
        for( Iterator<R> iterator = targets.iterator(); iterator.hasNext()) {
            assemble(iterator.next(), cacheService);
        }
        return targets;
    }
}

类信息封装对象

public class DictBeanInfo {
    public static class DictBeanFieldInfo {
        private String fieldName;
        private Field field;
        private String category;
        private String idFieldName;
        private Field idField;
    }
    
    private String className;
    private Class<? extends Object> clazz;
    private Map<String, DictBeanFieldInfo> fieldInfos;
    
}

Spring 容器后处理器加载注解的类

处理流程:
1 从Spring的容器,获得所有注解了DictClassAnno的类
2 取出有注解的类的所有属性。注意,这里取出的属性,不包括父类继承来的属性。
3 遍历属性,如果有注解DictFieldAnno,则解析信息,缓存到DictBeanInfo中。这里需要缓存两个field:一个是有注解的字段,表名这个字段需要填充字典值;另外一个是code字段,提供了取值的key。

注意:ReflectionUtils是Spring core中的工具,findField()方法会在当前类和所有的父类中查找field。

@Component
public class DictPostProcessor implements BeanFactoryPostProcessor{
    
    @Override
    public void postProcessBeanFactory(ConfiturableListableBeanFactory beanFactory) throws BeansException {
        Map<String, Object> beanWithAnnotation = beanFactory.getBeansWithAnnotation(DictClassAnno.class);
        if (beanWithAnnotation != null) {
            for( Iterator<Entry<String, Object>> iterator = beansWithAnnotation.entrySet().iterator(); iterator.hasNext();) {
                Entry<String, Object> entry = iterator.next();
                Class<? extends Object> beanClass = entry.getValue().getClass();
                Field[] fields = beanClass.getDeclaredFields();
                DictBeanInfo beanInfo = new DictBeanInfo(beanClass.getSimpleName(), beanClass);
                for(Field field: fields) {
                    try {
                        DictFieldAnno annotation = field.getAnnotation(DictFieldAnno.class);
                        if(annotation != null) {
                            Field idField = ReflectionUtils.findField(beanClass, annotation.field());
                            ReflectionUtils.makeAccessible(field);
                            ReflectionUtils.makeAccessible(idField);
                            DictBeanFieldInfo fieldInfo = new DictBeanFieldInfo(field.getName(), field, annotation.field(), idField, annotation.category());
                            beanInfo.getFieldInfos().put(field.getName(), fieldInfo);
                        }
                    }catch (Exception e) {
                        log.warn("", e);
                    }
                }
                DictAssembler.add(beanClass.getSimpleName(), beanInfo);
            }
        }
    }
}

字典数据缓存

缓存方案

  • 字典数据是通过category分组的,每个组一个key存放在Redis中
  • 每个请求中,相同的字典数据,只向Redis访问一次。
  • Redis的数据定期过期。
  • Redis数据过期,则通过数据库查询,并维护到Redis中。

代码实现

@Service
@Scope(ConfigurableBeanFacotry.SCOPE_PROTOTYPE)
public class DictCacheServiceImpl {
    private static final Long EXPIRE_TIME_MINUTES = 10L;
    
    @Autowired
    private RedissonClient redisson;
    
    @Autowired
    private DictDataService dictServcie;
    
    private Map<String, Map<String, Dict>> cache = new HashMap<>();
    
    public Map<String, Dict> get(String category) {
        if (! cahche.containsKey(category)) {
            RBucket<Object> bucket = redisson.getBucket(generateKey(category));
            List<Dict> datas = bucket.get();
            if ( datas != null) {
                List<Dict> dicts = dictService.getByCategory(category);
                bucket.set(dicts, EXPIRE_TIME_MINUTES, TimeUnit.MINUTES);
                cache.put(category, dicts.stream().collect(Collectors.toMap(Dict::getKey, data-> data)));
            } else {
                cache.put(category, datas.stream().collect(Collectors.toMap(Dict::getKey, data-> data)));
            }
        }
        return cache.get(category);
    }
    
    public String get(String category, String key) {
        Map<String, Dict> dict = get(category);
        if ( dict != null) {
            if( dict.containsKey(key)) {
                return dict.get(key).getCodeName();
            }
        }
        return null;
    }
    
    private String generateKey(String category) {
        return String.format("redis_key_%s", category);
    }
}

切面

@Aspect
@Component
public class DictServiceAspect {
    @Pointcur("execution(public * com.dce.webapp.service.impl.*ServiceImpl.*(..))")
    public void searchService() {
        
    }
    
    @AfterReturning(returning = "ret", pointcut = "searchService")
    public void doAfterReturning(Object ret) {
        if ( ret != null && ret instanceof List) {
            DictAssembler.assembleCollection(ret);
        }
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值