Skip to content

acuity-echo-starter自动回显

实现代码参考: acuity-echo-starter 模块.

使用场景

员工表中存储了岗位id(岗位表的主键)、职位状态(字典表的key)、民族(字典表的key)等字段,并在员工-部门关系表中存储了员工和部门的关联关系, 但在 列表页、详情页回显时,需要显示 岗位名称、职位状态、所属部门名称等。如下表:

ID名称岗位名称所属部门名称职位状态
1小王吧研发研发部,商务部离职

传统的解决思路

  1. 表关联查询
  2. 冗余字段
  3. 前端自己解决回显问题
  4. 先单表查询用户的数据,然后在java代码中,循环提取需要回显的station_id、字典key等, 在批量去查询这些信息后,在循环set到对应的字段中.

acuity解决方案

本项目提供了acuity-echo-starter模块来解决思路

  1. pom中加入依赖
  2. 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
  1. 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<>();
// ...
}
  1. 在 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;
  1. 由于base_employee表所在的服务,跟base_org、base_position表在同一个服务,但跟def_dict表不在同一个服务 。所以回显岗位和部门可以在@Echo 的 api 字段使用 ManagerImpl ,但回显字典需要在@Echo 的 api 字段使用 Feign
  2. 新增 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;
    }
}
  1. 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 {}
  1. 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 实现参考源码
  1. 然后在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);
 }
  1. 调用该接口后,会在User的echoMap中put需要显示的值。

实现原理

  1. 项目启动时,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);
}
  1. 项目运行时, 调用 echoService.action(obj) 方法并正常执行 action方法后,就会将 obj中的 echoMap填充。
  2. 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);
}
  1. 最后要自己实现LoadService的findByIds方法

性能解释

假设TestResultVO中有5个字段标记@Echo,分别是:orgIdList、orgIdList2、positionId、positionStatus、sex。 其中 orgIdListorgIdList2 都是查询 baseOrgManagerImplpositionStatussex都是查询 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#findByIdsbaseOrgManagerImpl#findByIdstop.acuity.box.oauth.api.DictionaryApi#findByIds 3个方法,每个方法传入多个Id或key,并将返回的数据存入 typeMap,待下一步写入回显数据

  • write:(循环标记了@Echo的字段数,本例中是5次)

    ClassManager#CACHE中缓存的字段来循环 写入需要回显的数据。

使用

  1. 跨服务回显时,@Echo中的api需要指定为 FeignClient 的全类名。

如:消息服务,需要回显权限服务的 用户名:@Echo(api="top.acuity.box.oauth.api.UserApi")

  1. 单个服务回显时,@Echo中的api需要指定为 ServiceImpl 的Spring id。

如:权限服务,需要回显权限服务的 用户名:@Echo(api="userServiceImpl")

3.枚举字段的回显,需要回显在字段上标记@Enum(api = Echo.ENUM_API)

欢迎使用天源云Saas快速开发系统