我们在项目的开发中,一般都会用到数据字典,但这有一点比较麻烦的是,数据库存的是数字,但在页面上进行展示的是汉字,通常的做法是把数字返给前端,然后前端再把数字进行转义;还有就是用SQL进行联表查询,把汉字查出来一起返给前端。其实还有更好的解决方案,那就是用自定义注解+Aop
先来看表设计:
t_annotation_data_dict表
t_annotation_data_item表
t_annotation_student表
现在要做的效果就是不用联表查询,将t_annotation_student表里用数字存放的字段相对应的汉字一起返给前端
相关的pom依赖:
com.alibaba
fastjson
1.2.4
相关准备工作代码:
application.yml
server:
servlet:
context-path: /annotation
port: 8080
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/demoSite1?useUnicode=true&characterEncoding=utf8
username: root
password: root
redis:
database: 10
host: 192.168.255.101
port: 6379
password: 123456
jedis:
pool:
max-active: 100
max-idle: 3
max-wait: -1
min-idle: 0
timeout: 10000
mybatis:
type-aliases-package: com.ue.mapper
mapper-locations: classpath:mapper/*.xml
configuration:
map-underscore-to-camel-case: true
logging:
level:
com.ue.mapper: debug
pagehelper:
helperDialect: mysql
reasonable: true
supportMethodsArguments: true
params: count=countSql
实体类代码
package com.ue.entity;
public class DataItem {
private Integer id;
private String datasource;
private String code;
private String val;
public DataItem(Integer id, String datasource, String code, String val) {
this.id = id;
this.datasource = datasource;
this.code = code;
this.val = val;
}
public DataItem() {
super();
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getDatasource() {
return datasource;
}
public void setDatasource(String datasource) {
this.datasource = datasource;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getVal() {
return val;
}
public void setVal(String val) {
this.val = val;
}
}
package com.ue.entity;
import com.ue.annotation.Dict;
public class Student {
private Integer id;
private String name;
@Dict(dictDataSource = "stu_level")
private Integer stuLevel;
@Dict(dictDataSource = "stu_english",dictText = "stuEnglishDictText")
private Integer englishLevel;
@Dict(dictDataSource = "stu_hobby")
private String stuHobby;
public Student(Integer id, String name, Integer stuLevel, Integer englishLevel, String stuHobby) {
this.id = id;
this.name = name;
this.stuLevel = stuLevel;
this.englishLevel = englishLevel;
this.stuHobby = stuHobby;
}
public Student() {
super();
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getStuLevel() {
return stuLevel;
}
public void setStuLevel(Integer stuLevel) {
this.stuLevel = stuLevel;
}
public Integer getEnglishLevel() {
return englishLevel;
}
public void setEnglishLevel(Integer englishLevel) {
this.englishLevel = englishLevel;
}
public String getStuHobby() {
return stuHobby;
}
public void setStuHobby(String stuHobby) {
this.stuHobby = stuHobby;
}
}
Mapper.java代码
package com.ue.mapper;
import com.ue.entity.DataItem;
import org.springframework.stereotype.Repository;
@Repository
public interface DataItemMapper {
/**
* 根据datasource与code查询字典的方法
* @author LiJun
* @Date 2019/11/28
* @Time 10:40
* @param dataItem
* @return com.ue.entity.DataItem
*/
DataItem selectByDatasourceCode(DataItem dataItem);
}
package com.ue.mapper;
import com.ue.entity.Student;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface StudentMapper {
List listPager(Student student);
}
service层代码
package com.ue.service;
import org.springframework.cache.annotation.Cacheable;
public interface DataItemService {
/**
* 将查询出来的字典文本值存到Redis里
* @author LiJun
* @Date 2019/11/28
* @Time 10:40
* @param dataSource
* @param key
* @return java.lang.String
*/
@Cacheable(value = "my-redis-cache2")
String selectByDatasourceKey(String dataSource, String key);
}
package com.ue.service;
import com.ue.entity.Student;
import com.ue.util.PageBean;
import java.util.List;
public interface StudentService {
List listPager(Student student);
}
service实现类代码
package com.ue.service.impl;
import com.ue.entity.DataItem;
import com.ue.mapper.DataItemMapper;
import com.ue.service.DataItemService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class DataItemServiceImpl implements DataItemService {
@Autowired
private DataItemMapper dataItemMapper;
@Override
public String selectByDatasourceKey(String dataSource, String key) {
DataItem dataItem = new DataItem();
dataItem.setDatasource(dataSource);
dataItem.setCode(key);
return dataItemMapper.selectByDatasourceCode(dataItem).getVal();
}
}
package com.ue.service.impl;
import com.ue.entity.Student;
import com.ue.mapper.StudentMapper;
import com.ue.service.StudentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class StudentServiceImpl implements StudentService {
@Autowired
private StudentMapper studentMapper;
@Override
public List listPager(Student student) {
return studentMapper.listPager(student);
}
}
Redis配置类代码
package com.ue.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.lang.reflect.Method;
import java.time.Duration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* redis配置类
* @author LiJun
* @Date 2019/11/27
* @Time 17:44
*/
@Configuration
@EnableCaching//开启注解式缓存
public class RedisConfig extends CachingConfigurerSupport {
/**
* 生成key的策略 根据类名+方法名+所有参数的值生成唯一的一个key
* @author LiJun
* @Date 2019/11/28
* @Time 9:12
* @param
* @return org.springframework.cache.interceptor.KeyGenerator
*/
@Bean
@Override
public KeyGenerator keyGenerator() {
return new KeyGenerator() {
@Override
public Object generate(Object target, Method method, Object... params) {
StringBuilder sb = new StringBuilder();
sb.append(target.getClass().getName());
sb.append(method.getName());
for (Object obj : params) {
sb.append(obj.toString());
}
return sb.toString();
}
};
}
/**
* 管理缓存
* @author LiJun
* @Date 2019/11/28
* @Time 9:12
* @param redisConnectionFactory
* @return org.springframework.cache.CacheManager
*/
@Bean
public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
//通过Spring提供的RedisCacheConfiguration类,构造一个自己的redis配置类,从该配置类中可以设置一些初始化的缓存命名空间
//及对应的默认过期时间等属性,再利用RedisCacheManager中的builder.build()的方式生成cacheManager:
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();//生成一个默认配置,通过config对象即可对缓存进行自定义配置
config = config.entryTtl(Duration.ofMinutes(1))//设置缓存的默认过期时间,也是使用Duration设置
.disableCachingNullValues();//不缓存空值
//设置一个初始化的缓存空间set集合
Set cacheNames = new HashSet<>();
cacheNames.add("my-redis-cache1");
cacheNames.add("my-redis-cache2");
//对每个缓存空间应用不同的配置
Map configMap = new HashMap<>();
configMap.put("my-redis-cache1", config);
configMap.put("my-redis-cache2", config.entryTtl(Duration.ofDays(3)));
RedisCacheManager cacheManager = RedisCacheManager.builder(redisConnectionFactory)//使用自定义的缓存配置初始化一个cacheManager
.initialCacheNames(cacheNames)//注意这两句的调用顺序,一定要先调用该方法设置初始化的缓存名,再初始化相关的配置
.withInitialCacheConfigurations(configMap)
.build();
return cacheManager;
}
@Bean
public RedisTemplate
重点代码:
studentController.java
package com.ue.controller;
import com.ue.entity.Student;
import com.ue.service.StudentService;
import com.ue.util.PageBean;
import com.ue.util.PageUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
import java.util.List;
@Controller
@RequestMapping("/student")
public class StudentController {
@Autowired
private StudentService studentService;
@ResponseBody
@RequestMapping("/listPager")
public PageUtils listPager(Student student, HttpServletRequest req){
PageBean pageBean = new PageBean();
pageBean.setRequest(req);
List list = this.studentService.listPager(student);
pageBean.setTotal(list.size());
PageUtils pageUtils = new PageUtils(list,pageBean.getTotal());
return pageUtils;
}
}
DictAspect.java
package com.ue.aspect;
import com.alibaba.fastjson.JSONObject;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ue.annotation.Dict;
import com.ue.service.DataItemService;
import com.ue.util.ObjConvertUtils;
import com.ue.util.PageUtils;
import com.ue.util.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.lang.reflect.Field;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* 用于翻译字典的切面类
* @author LiJun
* @Date 2019/11/28
* @Time 9:55
*/
@Aspect
@Component
@Slf4j
public class DictAspect {
private static String DICT_TEXT_SUFFIX = "_dictText";
@Autowired
private DataItemService dataItemService;
//定义切点Pointcut拦截所有对服务器的请求
@Pointcut("execution( * com.ue.controller.*.*(..))")
public void excudeService() {
}
/**
* 这是触发excudeService的时候会执行的,在环绕通知中目标对象方法被调用后的结果进行再处理
* @param pjp
* @return
* @throws Throwable
*/
@Around("excudeService()")
public Object doAround(ProceedingJoinPoint pjp) throws Throwable {
//这是定义开始事件
long time1 = System.currentTimeMillis();
//这是方法并获取返回结果
Object result = pjp.proceed();
//这是获取到结束时间
long time2 = System.currentTimeMillis();
log.info("获取JSON数据耗时:" + (time2 - time1) + "ms");
//解析开始时间
long start = System.currentTimeMillis();
//开始解析(翻译字段内部的值凡是打了@Dict这玩意的都会被翻译)
this.parseDictText(result);
//解析结束时间
long end = System.currentTimeMillis();
log.info("解析注入JSON数据耗时:" + (end - start) + "ms");
return result;
}
/**
* 本方法针对返回对象为Result的PageUtils的分页列表数据进行动态字典注入
* 字典注入实现 通过对实体类添加注解@Dict来标识需要的字典内容,字典分为单字典dataSource即可
* 示例为Student
* 字段为stu_sex添加了注解@Dict(dicDataSource = "stu_sex")会在字典服务立马查出来对应的text然后在请求list的时候将这个字典text,以字段名称加_dictText形式返回到前端
* 例如输入当前返回值的就会多出一个stu_sex_dictText字段
* {
* stu_sex:1,
* stu_sex_dictText:"男"
* }
* 前端直接取stu_sex_dictText的值在table里面进行展示,无需再进行前端的字典转换了
* customRender:function (text) {
* if(text==1){
* return "男";
* }else if(text==2){
* return "女";
* }else{
* return text;
* }
* }
* 目前vue是这么进行字典渲染到table上的多了就很麻烦了 这个直接在服务端渲染完成前端可以直接用
* @param result
*/
private void parseDictText(Object result) {
if (result instanceof PageUtils) {
List items = new ArrayList<>();
PageUtils pageUtils = (PageUtils) result;
//循环查找出来的数据
for (Object record : pageUtils.getData()) {
ObjectMapper mapper = new ObjectMapper();
String json = "{}";
try {
//解决@JsonFormat注解解析不了的问题详见SysAnnouncement类的@JsonFormat
json = mapper.writeValueAsString(record);
} catch (JsonProcessingException e) {
log.error("Json解析失败:" + e);
}
JSONObject item = JSONObject.parseObject(json);
//解决继承实体字段无法翻译问题
for (Field field : ObjConvertUtils.getAllFields(record)) {
//解决继承实体字段无法翻译问题
if (field.getAnnotation(Dict.class) != null) {//如果该属性上面有@Dict注解,则进行翻译
String datasource = field.getAnnotation(Dict.class).dictDataSource();//拿到注解的dictDataSource属性的值
String text = field.getAnnotation(Dict.class).dictText();//拿到注解的dictText属性的值
//获取当前带翻译的值
String key = String.valueOf(item.get(field.getName()));
//翻译字典值对应的text值
String textValue = translateDictValue(datasource, key);
//DICT_TEXT_SUFFIX的值为,是默认值:
//public static final String DICT_TEXT_SUFFIX = "_dictText";
log.debug("字典Val: " + textValue);
log.debug("翻译字典字段:" + field.getName() + DICT_TEXT_SUFFIX + ": " + textValue);
//如果给了文本名
if (!StringUtils.isBlank(text)) {
item.put(text, textValue);
} else {
//走默认策略
item.put(field.getName() + DICT_TEXT_SUFFIX, textValue);
}
}
//date类型默认转换string格式化日期
if (field.getType().getName().equals("java.util.Date") && field.getAnnotation(JsonFormat.class) == null && item.get(field.getName()) != null) {
SimpleDateFormat aDate = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
item.put(field.getName(), aDate.format(new Date((Long) item.get(field.getName()))));
}
}
items.add(item);
}
pageUtils.setData(items);
}
}
/**
* 翻译字典文本
* @param datasource
* @param key
* @return
*/
private String translateDictValue(String datasource, String key) {
//如果key为空直接返回就好了
if (ObjConvertUtils.isEmpty(key)) {
return null;
}
StringBuffer textValue = new StringBuffer();
//分割key值
String[] keys = key.split(",");
//循环keys中的所有值
for (String k : keys) {
String tmpValue = null;
log.debug("字典key:" + k);
if (k.trim().length() == 0) {
continue;//跳过循环
}
tmpValue = dataItemService.selectByDatasourceKey(datasource, k.trim());
if (tmpValue != null) {
if (!"".equals(textValue.toString())) {
textValue.append(",");
}
textValue.append(tmpValue);
}
}
//返回翻译的值
return textValue.toString();
}
}
Dict.java
package com.ue.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 专门用于数据字典中的数字转汉字的自定义注解
* @author LiJun
* @Date 2019/11/27
* @Time 16:08
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Dict {
/**
* 方法描述:数据dataSource
* @return 返回类型: String
*/
String dictDataSource();
/**
* 方法描述:这是返回后Put到josn中的文本key值
* @return 返回类型: String
*/
String dictText() default "";
}
PageUtils.java
package com.ue.util;
import java.io.Serializable;
import java.util.List;
public class PageUtils implements Serializable {
private static final long serialVersionUID = 1L;
//这是总行数
private long total;
//这是保持查询出来的数据
private List> data;
/**
* @param list 保存数据
* @param total 查到多行数据
*/
public PageUtils(List> list, long total) {
this.data = list;
this.total = total;
}
public long getTotal() {
return total;
}
public void setTotal(int total) {
this.total = total;
}
public List> getData() {
return data;
}
public void setData(List> data) {
this.data = data;
}
public static long getSerialVersionUID() {
return serialVersionUID;
}
}
ObjConvertUtils.java
package com.ue.util;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class ObjConvertUtils {
/**
* 获取类的所有属性,包括父类
* @param object
* @return
*/
public static Field[] getAllFields(Object object) {
Class> clazz = object.getClass();
List fieldList = new ArrayList<>();
while (clazz != null) {
fieldList.addAll(new ArrayList<>(Arrays.asList(clazz.getDeclaredFields())));
clazz = clazz.getSuperclass();
}
Field[] fields = new Field[fieldList.size()];
fieldList.toArray(fields);
return fields;
}
public static boolean isEmpty(Object object) {
if (object == null) {
return (true);
}
if ("".equals(object)) {
return (true);
}
if ("null".equals(object)) {
return (true);
}
return (false);
}
}
然后启动项目访问http://localhost:8080/annotation/student/listPager
返回的数据如下:
从下图中的打印可以看到如果不使用缓存技术的话,每次将数字转义为汉字的时候都会去查一次数据库,性能比较低
所以真正应用到项目中的话,还需要使用缓存技术,下图为使用了Redis将数据字典的数据进行缓存后的效果
源码地址:https://github.com/java-LJ/annotation-demo.git