问题描述
数据库的表中存放着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数据填充的方案
整体流程:
暴露的接口:
只需要在返回数据类型上添加注解就可以。
涉及的技术点:
- 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);
}
}
}