数据绑定注解功能
数据绑定注解
在实际开发中会遇到code转name的情况,一般情况下的写法就是先把数据查出来,然后再把数据中code对应的名称查出来,最后组装成需要的数据返回给前端展示,这种情况是没与问题的。但是无形之中增加了一些不必要代码显的有些臃肿,如果是在访问量比较高的接口还会影响一些性能。今天就记录一下利用缓存cache,反射写的一个数据绑定的注解,无需关注code转name的过程,仅需写出主要代码就可以了,剩下的用注解操作。非常方便,快捷。废话不多说,直接上案例:
可以看到,数据绑定相同条件下第二次请求直接走缓存不查库,可以提高效率。
接下来上代码:
注解定义:
package com.example.test1.databind.aspect.annotation;import com.example.test1.databind.property.BaseFastDataBindProperty;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;/*** @Author xx* @Date 2024/6/28 9:11* @Description: 字段数据快速绑定* @Version 1.0*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface FastDataBindFiled {/*** 指定数据绑定配置类*/Class<? extends BaseFastDataBindProperty> config();/*** 对应数据库表查询字段*/String conditionColumn() default "";/*** 条件字段例:要查字zt = 3的名称 这里传入zt*/String conditionProperty() default "";/*** 获取结果值的表字段*/String valueColumn() default "";/*** sql中拼接 and的语句*/String andCondition() default "";
}
package com.example.test1.databind.aspect.annotation;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;/*** @Author xx* @Date 2024/6/28 9:08* @Description: 快速数据绑定方法切入* @Version 1.0*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface FastDataBindResult {/*** 深度查询次数默认1*/int deepQueryTimes() default 1;
}
注解生效的切面类:
package com.example.test1.databind.aspect;import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.example.test1.databind.aspect.annotation.FastDataBindFiled;
import com.example.test1.databind.aspect.annotation.FastDataBindResult;
import com.example.test1.databind.bind.FastDataBind;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;/*** @Author xx* @Date 2024/6/28 9:02* @Description: 快速数据绑定切面类* @Version 1.0*/
@Aspect
@Component
public class DataBindAspect {@Around(value = "@annotation(fastDataBindResult)")public Object handle(ProceedingJoinPoint joinPoint, FastDataBindResult fastDataBindResult) {Object result = null;try {result = joinPoint.proceed();//探测次数for (int i = 0; i < fastDataBindResult.deepQueryTimes(); i++) {result = handleDataBindResult(result);}} catch (Throwable e) {throw new RuntimeException(e.getMessage(), e);}return result;}@Around(value = "@annotation(fastDataBindFiled)")public Object handle(ProceedingJoinPoint joinPoint, FastDataBindFiled fastDataBindFiled) {return handleDataBindResult(joinPoint);}private Object handleDataBindResult(Object data) {Object dataBindData;if (data instanceof Page) {Page<?> page = (Page<?>) data;dataBindData = page.getRecords();} else {dataBindData = data;}FastDataBind.dataBind(dataBindData);return data;}
}
具体实现逻辑
package com.example.test1.databind.bind;import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.annotation.TableName;
import com.example.test1.databind.aspect.annotation.FastDataBindFiled;
import com.example.test1.databind.property.BaseFastDataBindProperty;
import com.example.test1.databind.property.FastDataBindProperty;
import com.example.test1.databind.sql.SelectSqlBuilder;
import com.example.test1.util.SpElUtils;
import com.google.common.base.Joiner;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import lombok.Data;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.expression.EvaluationContext;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StopWatch;import javax.annotation.PostConstruct;
import java.lang.reflect.Field;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;import static cn.hutool.core.text.CharPool.COLON;
import static com.baomidou.mybatisplus.core.enums.SqlKeyword.AND;
import static com.baomidou.mybatisplus.core.toolkit.StringPool.DASH;
import static com.baomidou.mybatisplus.core.toolkit.StringPool.SINGLE_QUOTE;/*** @Author xx* @Date 2024/6/28 10:17* @Description: 快速数据绑定* @Version 1.0*/
@Slf4j
@Component
public class FastDataBind {/*** map<类,类中数据绑定的字段>*/private static final Map<Class<?>, Field[]> CLASS_DATABIND_FIELD_MAP = new ConcurrentHashMap<>(256);private static final Joiner COLON_JOINER = Joiner.on(COLON);public static final String LOGIN_TENANT_SHARED_TYPE = "loginTenantId";public static final String LOGIN_USER_SHARED_TYPE = "loginUserId";/*** 本地缓存,重启服务时会清空*/private static final Cache<String, DataBindCollector> CACHE = CacheBuilder.newBuilder()//过期时间.expireAfterAccess(10L, TimeUnit.SECONDS)//缓存容量大小.initialCapacity(128).build();/*** 数据绑定** @param data 目标属性带有{@link com.example.test1.databind.aspect.annotation.FastDataBindFiled}注解,将做数据绑定*/public static void dataBind(Object data) {if (data == null) {return;}Map<String, List<DataBindCollector>> collGroup;StopWatch stopWatch = new StopWatch();stopWatch.start();Field[] fields;if (data instanceof Collection) {Collection<?> list = (Collection<?>) data;if (CollUtil.isEmpty(list)) {return;}//查找出带有数据绑定注解的字段fields = list.stream().findFirst().map(FastDataBind::detectFields).orElse(null);if(fields == null){return;}//构建并为收集器做分组collGroup = list.parallelStream().flatMap(tmp -> buildCollectors(tmp).stream()).collect(Collectors.toList()).stream().collect(Collectors.groupingBy(DataBindCollector::groupKey));} else {fields = detectFields(data);collGroup = buildCollectors(data).stream().collect(Collectors.groupingBy(DataBindCollector::groupKey));}if (CollUtil.isEmpty(collGroup)) {return;}//根据分组数据做查询绑定dataBindGroup(collGroup);stopWatch.stop();log.info("======================数据绑定共耗时:{} ms===========================", stopWatch.getTotalTimeMillis());}/*** 根据数据绑定收集器进行数据绑定*/private static void dataBindGroup(Map<String, List<DataBindCollector>> collGroup) {for (Map.Entry<String, List<DataBindCollector>> entry : collGroup.entrySet()) {String key = entry.getKey();List<DataBindCollector> valueList = entry.getValue();//保存已缓存的数据绑定收集器Map<String, DataBindCollector> tempCache = new HashMap<>(64);//记录and拼接条件String[] andSql = {""};List<String> inValues = valueList.stream().filter(tmp -> {String unionKey = tmp.unionKey();//判断是否有缓存,有则从缓存中取DataBindCollector cache = CACHE.getIfPresent(unionKey);if (cache != null) {tempCache.put(unionKey, cache);return false;}boolean result = tmp.getConditionValue() != null;if (result) {andSql[0] = tmp.andCondition;}return result;}).map(collector -> parseForString(collector.getConditionValue())).distinct().collect(Collectors.toList());//从数据库查询值Map<String, Map<String, Object>> columnDataMap = queryDataMap(key,inValues,andSql[0]);if (CollUtil.isEmpty(columnDataMap) && CollUtil.isEmpty(tempCache)) {continue;}for (DataBindCollector collector : valueList) {// 如果有缓存,优先从缓存获取String unionKey = collector.unionKey();String conditionValue = String.valueOf(collector.getConditionValue());// 从缓存中获取结果值if (tempCache.containsKey(unionKey)) {DataBindCollector cache = tempCache.get(unionKey);// 设置查询出来的结果值到收集器collector.setValue(cache.getValue());}// 从数据库查询获取结果值else if (columnDataMap.containsKey(conditionValue)) {Map<String, Object> dataMap = columnDataMap.get(conditionValue);// 设置查询出来的结果值到收集器String upperCaseValueColumn = collector.getValueColumn().toUpperCase();String lowerCaseValueColumn = collector.getValueColumn().toLowerCase();Object value = Optional.ofNullable(dataMap.get(upperCaseValueColumn)).orElse(dataMap.get(lowerCaseValueColumn));collector.setValue(value);} else {continue;}// 放入到有过期时间的缓存当中去CACHE.put(unionKey, collector);// 为对象进行结果值的数据绑定collector.dataBind();}}}private static Map<String, Map<String, Object>> queryDataMap(String key, List<String> inValues, String andSql) {if(CollUtil.isEmpty(inValues)){return Collections.emptyMap();}// 分组的key组成公式:表名 + 查询的数据库表字段。详见内部类DataBindCollector的groupKey方法String[] tableColumnPair = key.split(String.valueOf(COLON));// 构建批量查询SQLSelectSqlBuilder selectSqlBuilder = new SelectSqlBuilder(tableColumnPair[0]).in(tableColumnPair[1], inValues);if (StrUtil.isNotBlank(andSql)) {selectSqlBuilder.lastSql(AND + andSql);}String sql = selectSqlBuilder.build();// 执行之前,进行SQL后置处理:例如数据权限的过滤处理
// sql = SimpleSqlHandlerChain.handle(sql);log.info("[数据绑定查询sql为:] - {}", sql);// 表条件字段和行数据的映射return fastDataBindMapper().queryForList(sql).stream().collect(Collectors.toMap(map -> {String upperCaseKey = tableColumnPair[1].toUpperCase();String lowerCaseKey = tableColumnPair[1].toLowerCase();return String.valueOf(Optional.ofNullable(map.get(upperCaseKey)).orElse(map.get(lowerCaseKey)));}, m -> m, (oM, nM) -> oM));}/*** 查找需要数据绑定的字段* 查找以下三类字段:* 1:值为空且有数据绑定注解的注解* 2:值不为空,且为集合类* 3:值不为空且为bean** @param data 目标对象* @return 返回需要数据绑定的字段*/public static Field[] detectFields(Object data) {Class<?> objClass = data.getClass();//先找缓存if (CLASS_DATABIND_FIELD_MAP.containsKey(objClass)) {return CLASS_DATABIND_FIELD_MAP.get(objClass);}Field[] fields = Arrays.stream(ReflectUtil.getFields(objClass)).filter(tmp -> {tmp.setAccessible(true);try {Object value = tmp.get(data);//值为空且有数据绑定的注解boolean hasAnnotation = (value == null && hasAnnotation(tmp));//值不为空且为集合类boolean isColl = value != null && Arrays.asList(tmp.getType().getInterfaces()).contains(Collection.class);return hasAnnotation || isColl;} catch (IllegalAccessException e) {throw new RuntimeException(e.getMessage(), e);}}).toArray(Field[]::new);if (ArrayUtil.isEmpty(fields)) {return null;}//放入集合中CLASS_DATABIND_FIELD_MAP.put(objClass, fields);return fields;}private static boolean hasAnnotation(Field field) {return field.isAnnotationPresent(FastDataBindFiled.class);}private static List<DataBindCollector> buildCollectors(Object data) {if (data == null) {return Collections.emptyList();}//获取可能需要数据绑定的字段Field[] fields = detectFields(data);if (fields == null) {return Collections.emptyList();}List<DataBindCollector> dataBindCollectors = new ArrayList<>();for (Field field : fields) {try {field.setAccessible(true);//过滤调没有数据绑定的注解或置为空的过滤掉Object value = field.get(data);if (Arrays.asList(field.getType().getInterfaces()).contains(Collection.class)) {if (value == null) {continue;}Collection<?> dataValueColl = (Collection<?>) value;if (CollUtil.isEmpty(dataValueColl)) {continue;}Field[] dataValueFields = detectFields(dataValueColl.stream().iterator().hasNext());if (ArrayUtil.isNotEmpty(dataValueFields)) {dataBindCollectors.addAll(dataValueColl.stream().flatMap(tmp -> buildCollectors(tmp).stream()).collect(Collectors.toList()));}} else if (value == null) {FastDataBindProperty fastDataBindProperty = getFastDataBindProperty(field);DataBindCollector bindCollector = new DataBindCollector(fastDataBindProperty, field, data, null, null);if (bindCollector.conditionValue != null) {dataBindCollectors.add(bindCollector);}} else {Field[] detectFields = detectFields(data);if (ArrayUtil.isNotEmpty(detectFields)) {dataBindCollectors.addAll(buildCollectors(detectFields));}}} catch (IllegalAccessException e) {throw new RuntimeException(e);}}return dataBindCollectors;}/*** 解析字段值为符合mysql的字符串形式** @param val 字段值* @return 符合mysql的字符串形式*/private static String parseForString(Object val) {if (val == null || val instanceof Number || val instanceof Boolean) {return String.valueOf(val);}if (val instanceof String) {String varStr = val.toString();if (varStr.startsWith(SINGLE_QUOTE) && varStr.endsWith(SINGLE_QUOTE)) {return val.toString();}}return "'" + val + "'";}/*** 获取字段上数据绑定配置数据*/private static FastDataBindProperty getFastDataBindProperty(Field field) {FastDataBindProperty property = null;if (field.isAnnotationPresent(FastDataBindFiled.class)) {FastDataBindFiled dataBindFiled = field.getDeclaredAnnotation(FastDataBindFiled.class);Optional<FastDataBindProperty> fastDataBindPropertyOpt = fastDataBindPropertyFactory(dataBindFiled.config());if (fastDataBindPropertyOpt.isPresent()) {property = fastDataBindPropertyOpt.get();}String valueColumn = dataBindFiled.valueColumn();if (StringUtils.isNotBlank(valueColumn)) {Optional.ofNullable(property).ifPresent(p -> p.setValueColumn(valueColumn));}String conditionProperty = dataBindFiled.conditionProperty();if (StringUtils.isNotBlank(conditionProperty)) {Optional.ofNullable(property).ifPresent(p -> p.setConditionProperty(conditionProperty));}String conditionColumn = dataBindFiled.conditionColumn();if (StringUtils.isNotBlank(conditionColumn)) {Optional.ofNullable(property).ifPresent(p