简单的controller响应体码值转换
1. 需求
服务整体架构为微服务集群架构,模块为oracle数据库不同用户,码值信息缓存于公共用户中,其他服务想要从数据库中获取码值比较费劲,所以就建立了一个专门的缓存码值管理模块,cache模块,将码值按照固定格式存储于redis中,方便所有微服务模块获取。有很多页面需要进行码值转换,列表信息数据库中存储的往往是码值的值,但是显示的时候想要显示其描述信息,所以我写了一个切面专门用于page列表的码值转换,让转换更加方便。
2.具体实现过程
这个功能实现是有具体要求的,我们系统是前后端分离,所以我们响应信息为固定对象Result格式,具体包含data,code,message三个属性,具体实现步骤如下:
- 1.定义好切面注解TableCode,其中有keyNameMapping字段,列表信息column值和codeTypeCd对应关系
- 2.定义好切面,拦截需要转义的方法,拦截响应信息,获取Result中对象的pageBean对象,从缓存组件中获取缓存信息,进行码值转义
3. 编码部分如下
3.1 切面注解格式如下
切面注解主要定义好返回pageBean对象中,哪些列需要进行转化,对应要转化为码值的哪些值
package cn.git.common.annotation;
import java.lang.annotation.*;
/**
* 列表页面返回信息处理
* @program: bank-credit-sy
* @author: lixuchun
* @create: 2021-06-30
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TableCode {
/**
* 列表信息column值和codeTypeCd对应关系
* 如下: pageBean对象第一列为客户类型码值,第二列为国家码值
* eg: typeCd=customerTypeCd,nation=NationCd
* @return 对应关系串
*/
String keyNameMapping() default "";
/**
* 非必填项
* 注解是否风险使用,默认否
* 如果风险使用 则码表获取从风险码表中获取
*/
boolean ifRisk() default false;
}
3.2 列表转换切面实现类
此类中主要通过缓存平台获取码值信息,再获取响应信息中Result的page类型对象,进行转义,我们系统是双系统共用通用组件,有三个系统,其中风险单独,押品以及信贷共用一套码值,所以系统转义需要区分是否为风险,如果是风险需要获取风险码值信息。
package cn.git.common.aspect;
import cn.git.common.annotation.TableCode;
import cn.git.common.constant.CommonConstant;
import cn.git.common.exception.ServiceException;
import cn.git.common.page.PageBean;
import cn.git.redis.RedisUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.lang.reflect.Field;
import java.util.*;
import java.util.stream.Collectors;
/**
* 列表码值转换切面类
* @program: bank-credit-sy
* @author: lixuchun
* @create: 2021-06-30
*/
@Aspect
@Slf4j
@Component
public class TableCodeTypeAspect {
@Autowired
private RedisUtil redisUtil;
/**
* 处理返回结果,根据注解中的配置转换数据中的codeTypeCd。
* @param joinPoint 切面的连接点,用于获取方法等信息。
* @param result 方法返回的结果对象。
* @param tableCode 注解类,用于获取注解上的配置信息。
*/
@AfterReturning(pointcut = "@annotation(tableCode)", returning = "result")
public void pointCut(JoinPoint joinPoint, Object result, TableCode tableCode) {
try {
// 判断是否处理风险数据
boolean ifRisk = tableCode.ifRisk();
// 处理列与codeTypeCd的映射关系
if (StrUtil.isNotBlank(tableCode.keyNameMapping())
&& tableCode.keyNameMapping().split(CommonConstant.EQUAL_SPLIT_FLAG).length
>= CommonConstant.MIN_SIZE) {
// 将映射关系字符串转换为List<Map<String, String>>
List<Map<String, String>> mappingList = Arrays.stream(tableCode.keyNameMapping()
.split(CommonConstant.COMMA_FLAG))
.map(mappingStr -> {
String[] mappingArray = mappingStr.split(CommonConstant.EQUAL_SPLIT_FLAG);
Map<String, String> mappingMap = new HashMap<>(CommonConstant.DEFAULT_MAP_SIZE);
mappingMap.put(mappingArray[0], mappingArray[1]);
return mappingMap;
}).collect(Collectors.toList());
// 根据映射关系和是否处理风险数据,获取code名称的缓存信息
Map<String, String> cacheMap = getCodeName(mappingList, ifRisk);
// 处理返回结果对象,转换其中的codeTypeCd
Field[] fields = result.getClass().getDeclaredFields();
// 修改访问权限以访问私有字段
boolean accessFlag = fields[2].isAccessible();
fields[2].setAccessible(true);
Object outVO = fields[2].get(result);
if (ObjectUtil.isNotEmpty(outVO)) {
// 在outVO中找到PageBean对象,并处理其中的result数据
Field[] outVOFields = outVO.getClass().getDeclaredFields();
Field pageBeanField = Arrays.stream(outVOFields).filter(localField -> {
boolean checkResult = false;
try {
boolean voAccessFlag = localField.isAccessible();
localField.setAccessible(true);
Object outVOFieldValue = localField.get(outVO);
checkResult = outVOFieldValue instanceof PageBean;
localField.setAccessible(voAccessFlag);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return checkResult;
}).findAny().orElse(null);
if (ObjectUtil.isNotNull(pageBeanField)) {
// 获取PageBean对象并处理其中的result数据
boolean pageBeanAccessible = pageBeanField.isAccessible();
pageBeanField.setAccessible(true);
Object pageBean = pageBeanField.get(outVO);
// 在PageBean中找到result字段并处理
Field[] pageBeanFields = pageBean.getClass().getDeclaredFields();
Field resultField = Arrays.stream(pageBeanFields).filter(localField -> {
String fieldName = localField.getName();
if (CommonConstant.RESULT_FLAG.equals(fieldName)) {
return true;
}
return false;
}).findAny().orElse(null);
boolean resultFieldAccessible = resultField.isAccessible();
resultField.setAccessible(true);
Object pageBeanResult = resultField.get(pageBean);
// 如果result不为空,则进行转换处理
if (ObjectUtil.isNotEmpty(pageBeanResult)) {
pageBeanResult = convertPageBeanCodeTypeCd(pageBeanResult, mappingList, cacheMap);
}
// 恢复访问权限
resultField.setAccessible(resultFieldAccessible);
pageBeanField.setAccessible(pageBeanAccessible);
fields[2].setAccessible(accessFlag);
}
}
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
/**
* 处理pageBean中result对应的codeTypeCd转换成codeName
* @param pageBeanResult 结果集,预期为一个对象列表,这些对象需要进行码值转换
* @param mappingList 关联对应关系列表,用于指示需要进行转换的字段及其对应的码表类型代码
* @param cacheMap 缓存的码表名称到码值的映射,用于快速查找码值对应的名称
* @return 转换完成的pageBeanResult对象,其中的码值已替换为码名称
*/
public Object convertPageBeanCodeTypeCd(Object pageBeanResult,
List<Map<String, String>> mappingList,
Map<String, String> cacheMap) {
List<Object> resultList = (List<Object>) pageBeanResult;
for (Map<String, String> mappingMap : mappingList) {
List<String> entityColumnList = new ArrayList<>(mappingMap.keySet());
// 根据映射关系获取实体列名称和对应的系统码类型代码
String entityColumn = entityColumnList.get(0);
String sysCodeTypeCd = mappingMap.get(entityColumn);
// 遍历结果集,将码值转换为码名称
resultList = resultList.stream().map(result -> {
try {
// 获取并设置字段可访问,进行码值转换
Field field = result.getClass().getDeclaredField(entityColumn);
boolean accessible = field.isAccessible();
field.setAccessible(true);
String codeCd = StrUtil.toString(field.get(result));
field.set(result, cacheMap.get(StrUtil.format(CommonConstant.TWO_STR_APPEND_TEMPLATE, sysCodeTypeCd,
codeCd)));
// 恢复字段访问性设置
field.setAccessible(accessible);
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
throw new ServiceException("PageBean对象转换缓存信息转换异常!");
}
return result;
}).collect(Collectors.toList());
}
return resultList;
}
/**
* 通过codeTypeCd查询缓存列表信息,转换为Map格式
* key: codeTypeCd:codeCd value: codeName
* @param mappingList 映射列表,用于指示需要查询的码表类型代码
* @param ifRisk 是否为风险相关码表
* @return 码表名称到码值的映射,用于码值转换
*/
public Map<String, String> getCodeName(List<Map<String, String>> mappingList, boolean ifRisk) {
Map<String, String> finalMap = new HashMap<>(CommonConstant.DEFAULT_MAP_SIZE);
for (Map<String, String> stringStringMap : mappingList) {
List<String> entityColumnList = new ArrayList<>(stringStringMap.keySet());
// 获取列名称和系统码类型代码
String entityColumn = entityColumnList.get(0);
String sysCodeTypeCd = stringStringMap.get(entityColumn);
// 根据是否风险相关查询不同的码表缓存
List<JSONObject> cacheJsonObject;
if (ifRisk) {
// 风险相关码表查询
if (ObjectUtil.isEmpty(redisUtil.hget(CommonConstant.TABLE_RISK_TC_SYS_CODES, sysCodeTypeCd))) {
throw new ServiceException("分页列表码值查询为空,请正确传入@TableCode中参数!");
}
cacheJsonObject = ((List<Object>)
redisUtil.hget(CommonConstant.TABLE_RISK_TC_SYS_CODES, sysCodeTypeCd)).stream().map(cache->
(JSONObject)JSON.toJSON(cache)
).collect(Collectors.toList());
} else {
// 非风险相关码表查询
if (ObjectUtil.isEmpty(redisUtil.hget(CommonConstant.TABLE_TC_SYS_CODES, sysCodeTypeCd))) {
throw new ServiceException("分页列表码值查询为空,请正确传入@TableCode中参数!");
}
cacheJsonObject = ((List<Object>)
redisUtil.hget(CommonConstant.TABLE_TC_SYS_CODES, sysCodeTypeCd)).stream().map(cache->
(JSONObject)JSON.toJSON(cache)
).collect(Collectors.toList());
}
// 处理查询结果,转换为名称到码值的映射
for (JSONObject cacheTcSysCodes : cacheJsonObject) {
Map<String, String> resultMap = new HashMap<>(CommonConstant.DEFAULT_MAP_SIZE);
resultMap.put(StrUtil.format(CommonConstant.TWO_STR_APPEND_TEMPLATE, sysCodeTypeCd,
cacheTcSysCodes.getString(CommonConstant.CODE_CD_FLAG)),
cacheTcSysCodes.getString(CommonConstant.CODE_NAME_FLAG));
finalMap.putAll(resultMap);
}
}
return finalMap;
}
}
3.3 码值格式
码值从数据库读取,然后存储到redis中
3.3.1 代码部分格式
我的码值存储于redis中,并且以hash格式存储,总体格式:hash tableName codeTypeCd list,其中CacheTcSysCodes为数据库对应对象类型,具体内容如下:
package cn.git.cache.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* 字典表带码值数据
* @program: bank-credit-sy
* @author: lixuchun
* @create: 2021-04-12
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CacheTcSysCodes {
/**
* 码值主键
*/
private String codeId;
/**
* 父键值
*/
private String parentCodeId;
/**
* 码值级别
*/
private String codeLevelCd;
/**
* 码值value
*/
private String codeCd;
/**
* 码值key
*/
private String codeKey;
/**
* 码值名称
*/
private String codeName;
/**
* 码值类型cd
*/
private String codeTypeCd;
/**
* 码值排序列
*/
private String orderNo;
/**
* 默认Ind
*/
private String defaultInd;
/**
* 对应codeId子集
*/
private List<CacheTcSysCodes> tcSysCodesList;
}
3.3.2 redis缓存存储格式
最终存储到redis中格式如下:
3.4 代码controller调用部分格式
4. 测试部分
此处返回对的逾期,对应codeTypeCd类型为ContractStatusCd,具体内容可以参照上面redis中存储的格式,响应pageBean对象中,查询数据库的contractStatusCd值为5,对应缓存中的codeCd值应该是5,通过转义后codeName展示信息为逾期,其余码值转换类似。