Appearance
acuity-echo-starter自动回显
实现代码参考: acuity-echo-starter 模块.
使用场景
员工表中存储了岗位id(岗位表的主键)、职位状态(字典表的key)、民族(字典表的key)等字段,并在员工-部门关系表中存储了员工和部门的关联关系, 但在 列表页、详情页回显时,需要显示 岗位名称、职位状态、所属部门名称等。如下表:
ID | 名称 | 岗位名称 | 所属部门名称 | 职位状态 |
---|---|---|---|---|
1 | 小王吧 | 研发 | 研发部,商务部 | 离职 |
传统的解决思路
- 表关联查询
- 冗余字段
- 前端自己解决回显问题
- 先单表查询用户的数据,然后在java代码中,循环提取需要回显的station_id、字典key等, 在批量去查询这些信息后,在循环set到对应的字段中.
acuity解决方案
本项目提供了acuity-echo-starter模块来解决思路。
- pom中加入依赖
- yml 开启配置, 更多更详细的配置参考
EchoProperties
yaml
acuity:
echo:
# 是否启用 @EchoResult
aop-enabled: true
# 启动程序时,将此包下标记了@Echo注解的实体类缓存到内存,提高回显性能
basePackages: 'top.acuity'
# 字典类型 和 code 的分隔符
dictSeparator: '###'
# 多个字典code 之间的的分隔符
dictItemSeparator: ','
# 递归最大深度
maxDepth: 3
acuity:
echo:
# 是否启用 @EchoResult
aop-enabled: true
# 启动程序时,将此包下标记了@Echo注解的实体类缓存到内存,提高回显性能
basePackages: 'top.acuity'
# 字典类型 和 code 的分隔符
dictSeparator: '###'
# 多个字典code 之间的的分隔符
dictItemSeparator: ','
# 递归最大深度
maxDepth: 3
- EmployeeResultVO实体实现
EchoVO
接口,并在EmployeeResultVO中加入echoMap .
java
public class EmployeeResultVO extends Entity<Long> implements EchoVO {
@TableField(exist = false)
private Map<String, Object> echoMap = new HashMap<>();
// ...
}
public class EmployeeResultVO extends Entity<Long> implements EchoVO {
@TableField(exist = false)
private Map<String, Object> echoMap = new HashMap<>();
// ...
}
- 在 positionId, orgIdList, positionStatus 等需要回显的字段上标记注解: @Echo
java
// 等价于 @Echo(api = "basePositionManagerImpl")
@Echo(api = EchoApi.POSITION_ID_CLASS)
private Long positionId;
// 等价于 @Echo(api = "baseOrgManagerImpl")
@Echo(api = EchoApi.ORG_ID_CLASS)
private List<Long> orgIdList;
// 等价于 @Echo(api = "top.acuity.box.oauth.api.DictApi", dictType = DictionaryType.Base.POSITION_STATUS)
@Echo(api = EchoApi.DICTIONARY_ITEM_FEIGN_CLASS, dictType = EchoDictType.Base.POSITION_STATUS)
private String positionStatus;
@Echo(api = Echo.ENUM_API)
private String sex;
// 等价于 @Echo(api = "basePositionManagerImpl")
@Echo(api = EchoApi.POSITION_ID_CLASS)
private Long positionId;
// 等价于 @Echo(api = "baseOrgManagerImpl")
@Echo(api = EchoApi.ORG_ID_CLASS)
private List<Long> orgIdList;
// 等价于 @Echo(api = "top.acuity.box.oauth.api.DictApi", dictType = DictionaryType.Base.POSITION_STATUS)
@Echo(api = EchoApi.DICTIONARY_ITEM_FEIGN_CLASS, dictType = EchoDictType.Base.POSITION_STATUS)
private String positionStatus;
@Echo(api = Echo.ENUM_API)
private String sex;
- 由于base_employee表所在的服务,跟base_org、base_position表在同一个服务,但跟def_dict表不在同一个服务 。所以回显岗位和部门可以在@Echo 的 api 字段使用
ManagerImpl
,但回显字典需要在@Echo 的 api 字段使用Feign
。 - 新增
DictionaryApi
让它继承LoadService接口,并实现findByIds方法。
java
@FeignClient(name="acuity-oauth-server",path="/dict")
public interface DictionaryApi extends LoadService {
@Override
@GetMapping("/findByIds")
Map<Serializable, Object> findByIds(@RequestParam(value = "ids") Set<Serializable> ids);
}
@FeignClient(name="acuity-oauth-server",path="/dict")
public interface DictionaryApi extends LoadService {
@Override
@GetMapping("/findByIds")
Map<Serializable, Object> findByIds(@RequestParam(value = "ids") Set<Serializable> ids);
}
然后在oauth服务添加 feign 的实现,如(EchoController):
java
public class EchoController {
@PostMapping("/dict/findByIds")
public Map<Serializable, Object> findDictByIds(@RequestParam("ids") Set<Serializable> ids) {
// findByIds 接口需要自己实现
return this.dictService.findByIds(ids);
}
//....
}
public class EchoController {
@PostMapping("/dict/findByIds")
public Map<Serializable, Object> findDictByIds(@RequestParam("ids") Set<Serializable> ids) {
// findByIds 接口需要自己实现
return this.dictService.findByIds(ids);
}
//....
}
实现findByIds:
java
@Service
@RequiredArgsConstructor
public class DictServiceImpl implements DictService {
// 先查询base_dict,若base_dict无自定义字典,则查询def_dict
@Override
public Map<Serializable, Object> findByIds(Set<Serializable> dictKeys) {
if (CollUtil.isEmpty(dictKeys)) {
return Collections.emptyMap();
}
if (ContextUtil.isEmptyTenantId()) {
return defDictManager.findByIds(dictKeys);
}
Map<Serializable, Object> baseMap = baseDictManager.findByIds(dictKeys);
// dictKeys 数量和 baseMap.key 数量相同,说明所有的字典在base库都自定义了
if (baseMap != null && baseMap.keySet().size() == dictKeys.size()) {
return baseMap;
}
// 查询不在base的字典
Set<Serializable> nonExistKeys = dictKeys.stream().filter(dictKey -> !baseMap.containsKey(dictKey)).collect(Collectors.toSet());
Map<Serializable, Object> defMap = defDictManager.findByIds(nonExistKeys);
HashMap<Serializable, Object> map = MapUtil.newHashMap();
map.putAll(defMap);
map.putAll(baseMap);
return map;
}
}
@Service
@RequiredArgsConstructor
public class DictServiceImpl implements DictService {
// 先查询base_dict,若base_dict无自定义字典,则查询def_dict
@Override
public Map<Serializable, Object> findByIds(Set<Serializable> dictKeys) {
if (CollUtil.isEmpty(dictKeys)) {
return Collections.emptyMap();
}
if (ContextUtil.isEmptyTenantId()) {
return defDictManager.findByIds(dictKeys);
}
Map<Serializable, Object> baseMap = baseDictManager.findByIds(dictKeys);
// dictKeys 数量和 baseMap.key 数量相同,说明所有的字典在base库都自定义了
if (baseMap != null && baseMap.keySet().size() == dictKeys.size()) {
return baseMap;
}
// 查询不在base的字典
Set<Serializable> nonExistKeys = dictKeys.stream().filter(dictKey -> !baseMap.containsKey(dictKey)).collect(Collectors.toSet());
Map<Serializable, Object> defMap = defDictManager.findByIds(nonExistKeys);
HashMap<Serializable, Object> map = MapUtil.newHashMap();
map.putAll(defMap);
map.putAll(baseMap);
return map;
}
}
- manager 实现 LoadService
java
public interface BaseOrgManager extends SuperCacheManager<BaseOrg>, LoadService {}
public interface BasePositionManager extends SuperCacheManager<BasePosition>, LoadService {}
public interface BaseDictManager extends SuperCacheManager<BaseDict>, LoadService {}
public interface DefDictManager extends SuperCacheManager<DefDict>, LoadService {}
public interface BaseOrgManager extends SuperCacheManager<BaseOrg>, LoadService {}
public interface BasePositionManager extends SuperCacheManager<BasePosition>, LoadService {}
public interface BaseDictManager extends SuperCacheManager<BaseDict>, LoadService {}
public interface DefDictManager extends SuperCacheManager<DefDict>, LoadService {}
- ManagerImpl 实现具体逻辑
java
public class BaseOrgManagerImpl extends SuperCacheManagerImpl<BaseOrgMapper, BaseOrg> implements BaseOrgManager {
@Override
@Transactional(readOnly = true)
@DS(DsConstant.BASE_TENANT)
public Map<Serializable, Object> findByIds(Set<Serializable> params) {
if (CollUtil.isEmpty(params)) {
return Collections.emptyMap();
}
Set<Serializable> ids = new HashSet<>();
params.forEach(item -> {
if (item instanceof Collection) {
ids.addAll((Collection<? extends Serializable>) item);
} else {
ids.add(item);
}
});
List<BaseOrg> list = findByIds(ids,
missIds -> super.listByIds(missIds.stream().filter(Objects::nonNull).map(Convert::toLong).collect(Collectors.toList()))
);
return CollHelper.uniqueIndex(list, BaseOrg::getId, BaseOrg::getName);
}
}
// 其他的 managerImpl 实现参考源码
public class BaseOrgManagerImpl extends SuperCacheManagerImpl<BaseOrgMapper, BaseOrg> implements BaseOrgManager {
@Override
@Transactional(readOnly = true)
@DS(DsConstant.BASE_TENANT)
public Map<Serializable, Object> findByIds(Set<Serializable> params) {
if (CollUtil.isEmpty(params)) {
return Collections.emptyMap();
}
Set<Serializable> ids = new HashSet<>();
params.forEach(item -> {
if (item instanceof Collection) {
ids.addAll((Collection<? extends Serializable>) item);
} else {
ids.add(item);
}
});
List<BaseOrg> list = findByIds(ids,
missIds -> super.listByIds(missIds.stream().filter(Objects::nonNull).map(Convert::toLong).collect(Collectors.toList()))
);
return CollHelper.uniqueIndex(list, BaseOrg::getId, BaseOrg::getName);
}
}
// 其他的 managerImpl 实现参考源码
- 然后在BaseEmployeeController的page方法中,调用echoService.action
java
public R<IPage<BaseEmployeeResultVO>> page(@RequestBody @Validated PageParams<BaseEmployeePageQuery> params) {
IPage<BaseEmployeeResultVO> page = baseEmployeeBiz.findPageResultVO(params);
echoService.action(page);
return R.success(page);
}
public R<IPage<BaseEmployeeResultVO>> page(@RequestBody @Validated PageParams<BaseEmployeePageQuery> params) {
IPage<BaseEmployeeResultVO> page = baseEmployeeBiz.findPageResultVO(params);
echoService.action(page);
return R.success(page);
}
- 调用该接口后,会在User的echoMap中put需要显示的值。
实现原理
- 项目启动时,EchoService构造方法通过策略模式注入Map<String, LoadService> strategyMap
注意: 实现LoadService的类,必须被Spring 加载,即加了@Service、@Component等注解
java
public EchoServiceImpl(EchoProperties ips, Map<String, LoadService> strategyMap) {
/*
项目启动时,会利用Spring的构造器主动注入方式,将所有LoadService的实现类注入到strategyMap。
如:前面提到的: DictionaryApi、BaseOrgManagerImpl、BasePositionManagerImpl等
strategyMap的key是 @Echo注解中的api属性
strategyMap的value是 所有实现了LoadService接口的实现类 或 OpenFeign。
*/
this.strategyMap.putAll(strategyMap);
}
public EchoServiceImpl(EchoProperties ips, Map<String, LoadService> strategyMap) {
/*
项目启动时,会利用Spring的构造器主动注入方式,将所有LoadService的实现类注入到strategyMap。
如:前面提到的: DictionaryApi、BaseOrgManagerImpl、BasePositionManagerImpl等
strategyMap的key是 @Echo注解中的api属性
strategyMap的value是 所有实现了LoadService接口的实现类 或 OpenFeign。
*/
this.strategyMap.putAll(strategyMap);
}
- 项目运行时, 调用
echoService.action(obj)
方法并正常执行action
方法后,就会将obj
中的echoMap
填充。 echoService.action
方法逻辑如下:
若你自己写的代码echoMap没有填充,请认真理解并调试 action 方法!!!
java
/**
* 回显数据的3个步骤:(出现回显失败时,认真debug该方法)
* <p>
* 1. parse: 遍历obj对象的所有字段,并将标记了@Echo注解的字段存储到typeMap
* 2. load: 遍历typeMap,依次查询待回显的数据。此方法实际就是调用 LoadService.findByIds 方法查询数据。
* 3. write: 遍历obj对象的所有字段,将查询出来的结果通过反射写入 echoMap
* <p>
* 注意:若对象中需要回显的字段之间出现循环引用,很可能发生异常,所以请保证不要出现循环引用!!!
* 注意:若传入的参数是集合或IPage,并不会在循环中调用 LoadService.findByIds 方法。
*
* @param obj 需要回显的参数,支持 自定义对象(User)、集合(List<User>、Set<User>)、IPage<User>
* @param ignoreFields 忽略obj对象中的那些字段
*/
public void action(Object obj, String... ignoreFields) {
// 1
this.parse(obj, typeMap, 1, ignoreFields);
// 2
this.load(typeMap, isUseCache);
// 3
this.write(obj, typeMap, 1);
}
/**
* 回显数据的3个步骤:(出现回显失败时,认真debug该方法)
* <p>
* 1. parse: 遍历obj对象的所有字段,并将标记了@Echo注解的字段存储到typeMap
* 2. load: 遍历typeMap,依次查询待回显的数据。此方法实际就是调用 LoadService.findByIds 方法查询数据。
* 3. write: 遍历obj对象的所有字段,将查询出来的结果通过反射写入 echoMap
* <p>
* 注意:若对象中需要回显的字段之间出现循环引用,很可能发生异常,所以请保证不要出现循环引用!!!
* 注意:若传入的参数是集合或IPage,并不会在循环中调用 LoadService.findByIds 方法。
*
* @param obj 需要回显的参数,支持 自定义对象(User)、集合(List<User>、Set<User>)、IPage<User>
* @param ignoreFields 忽略obj对象中的那些字段
*/
public void action(Object obj, String... ignoreFields) {
// 1
this.parse(obj, typeMap, 1, ignoreFields);
// 2
this.load(typeMap, isUseCache);
// 3
this.write(obj, typeMap, 1);
}
- 最后要自己实现LoadService的findByIds方法
性能解释
假设TestResultVO中有5个字段标记@Echo,分别是:orgIdList、orgIdList2、positionId、positionStatus、sex。 其中 orgIdList
和 orgIdList2
都是查询 baseOrgManagerImpl
,positionStatus
和 sex
都是查询 top.acuity.box.oauth.api.DictionaryApi
java
public class TestResultVO implements EchoVO{
private Map<String, Object> echoMap = MapUtil.newHashMap();
// 其他字段 有8个...
@Echo(api = "basePositionManagerImpl")
private Long positionId;
@Echo(api = "baseOrgManagerImpl")
private List<Long> orgIdList;
@Echo(api = "baseOrgManagerImpl")
private List<Long> orgIdList2;
@Echo(api = "top.acuity.box.oauth.api.DictionaryApi", dictType = DictionaryType.Base.POSITION_STATUS)
private String positionStatus;
@Echo(api = "top.acuity.box.oauth.api.DictionaryApi", dictType = DictionaryType.Base.SEX)
private String sex;
}
public class TestResultVO implements EchoVO{
private Map<String, Object> echoMap = MapUtil.newHashMap();
// 其他字段 有8个...
@Echo(api = "basePositionManagerImpl")
private Long positionId;
@Echo(api = "baseOrgManagerImpl")
private List<Long> orgIdList;
@Echo(api = "baseOrgManagerImpl")
private List<Long> orgIdList2;
@Echo(api = "top.acuity.box.oauth.api.DictionaryApi", dictType = DictionaryType.Base.POSITION_STATUS)
private String positionStatus;
@Echo(api = "top.acuity.box.oauth.api.DictionaryApi", dictType = DictionaryType.Base.SEX)
private String sex;
}
在 action
方法中的循环次数为:
parse:(首次循环字段数,本例中是5 + 8次(8是因为其他字段有8个);第二次循环标记了@Echo的字段数,本例中是5次)
每个对象首次回显时,循环遍历TestResultVO的所有字段,并将字段中标记了@Echo注解的字段都缓存到
ClassManager#CACHE
,第二次遍历同一对象时,直接取ClassManager#CACHE
中缓存的字段来解析标记了@Echo的字段,并将字段的值写入typeMap
。load:(循环标记了@Echo的字段,并通过api去重后的次数,本例中是3次)
循环
typeMap
,依次调用basePositionManagerImpl#findByIds
、baseOrgManagerImpl#findByIds
、top.acuity.box.oauth.api.DictionaryApi#findByIds
3个方法,每个方法传入多个Id或key,并将返回的数据存入typeMap
,待下一步写入回显数据write:(循环标记了@Echo的字段数,本例中是5次)
取
ClassManager#CACHE
中缓存的字段来循环 写入需要回显的数据。
使用
- 跨服务回显时,@Echo中的api需要指定为 FeignClient 的全类名。
如:消息服务,需要回显权限服务的 用户名:@Echo(api="top.acuity.box.oauth.api.UserApi")
- 单个服务回显时,@Echo中的api需要指定为 ServiceImpl 的Spring id。
如:权限服务,需要回显权限服务的 用户名:@Echo(api="userServiceImpl")
3.枚举字段的回显,需要回显在字段上标记@Enum(api = Echo.ENUM_API)