一个典型的web应用中可能会有很多DAO接口(也被称作mapper,以下皆称mapper),在某些业务场景中,我们会对不同的mapper进行高度相似的业务操作,这种情况下如果仍然裸用mapper进行CRUD,就可能制造出大批高信息熵的代码,久而久之,难以维护.
例如在笔者之前所开发的项目,它是一个云产品监控系统,通过组合阿里云的blink、datahub、TableStore以及一些java微服务,实现了对几十种阿里云产品的数百个监控项的统一配置实时监控和按规则报警.
为了便于blink服务分监控项计算受监控实例的某项指标是否突破阈值,我们存储实例阈值配置的表结构被设计以实例ID作为唯一标识,横向扩展监控项的横表结构:
实例ID | 监控项1 | 监控项2 | 监控项3 | 更多监控项... |
实例1 | 55 | 66 | 77 | 88 |
实例2 | ||||
更多实例... |
并且,让每一种产品都对应一张阈值横表,便于维护,也避免了给不同产品的同名监控项起别名(比方说,ECS和RDS都有内存使用率这个监控项)维护的成本.
在这个场景中,负责报警配置的业务开发人员事实上并不需要关心每个监控项的具体业务含义,他可以对监控项作一个统一的抽象:
// 由于历史原因,我们的项目为每一个监控项都创建了对应的VO,
// 这样很繁琐,但是对于分类维护监控项或在domain层次针对性地扩展某个监控项则是有利的
// 如果你偏好贫血模型,那么可以尝试用一个CommonMonitorConfig描述所有
public abstract class CommonMonitorConfig {
// 超阈值状态超过此时间,则进行报警
String keepTime;
// 自上一次发送报警后的沉默周期
String silenceTime;
// 此监控项是否启用报警
Boolean isOpen;
// 监控项对应的字段名称
String monitorName;
// 配置阈值
Double value;
// 产品类型,贫血模型需要此字段
String productType;
// 充血模型可以定义此方法,让处于产品及的监控类例如EcsMonitorConfig实现此方法
abstract TypeEnum getTypeEnum();
public CommonMonitorConfig(){
// 如果使用充血模型,你可以分门别类的在构造方法中按类型初始化一些默认配置
}
}
前端可以根据此抽象制作操作报警配置的表单界面并提交报警配置数据.在约束好CommonMonitorConfig的monitorName与阈值配置实例类相应字段的映射关系后,后端接口中可以使用反射方便规整地将VO对象的相应值映射统一到对应的DO对象中.
当然,在真正实现丝滑流畅无污染的反射之前还需要作一点点合理封装.
以实例阈值配置的存储和查询为例,首先给所有产品的entity类定义一个公共父类,并提取公共业务字段到父类中,使得子类实体只存放阈值:
public class CommonInsAlarmRule {
@TableId
public Long id;
/**
* 规则编码
*/
public String ruleUUID;
/**
* 实例ID
*/
public String instanceId;
public String moduleCode;
}
然后,写一个ConfigUtil维护一些静态映射关系并封装常用方法:
/**
* k: 产品类别
* v: 报警规则实体类
*/
public static final Map<TypeEnum, Class<? extends CommonInsAlarmRule>> insClassMap = new ImmutableMap
.Builder<CloudProductTypeEnum, Class<? extends CommonInsAlarmRule>>()
.put(ECS, EcsAlarmRule.class)
.put(EIP, EipAlarmRule.class)
.put(RDS, RdsAlarmRule.class)
// ....
.build();
// 比方说,从上面的字典中获取insType,然后将其实例化为被标记为CommonInsAlarmRule的具体子类对象
public static <P, S extends P> P newInstance(Class<S> insType) {
try {
return insType.newInstance();
} catch (InstantiationException | IllegalAccessException e) {
}
}
// 这个方法直接定义在TypeEnum这个枚举类里
public static TypeEnum findEnumByNameIgnoreCase(String displayName){
return Arrays.stream(TypeEnum .values()).filter(e -> e.displayName.equalsIgnoreCase(displayName)).findFirst().orElse(null);
}
同样地,在ConfigUtil中注入并初始话各个产品mapper对象(这里以MybatisPlus为例)的映射关系:
private static Map<String, BaseMapper<? extends CommonInsAlarmRule>> instanceAlarmRuleMapperMap;
private final EcsAlarmRuleMapper ecsAlarmRuleMapper;
private final RdsAlarmRuleMapper rdsAlarmRuleMapper;
private final EipAlarmRuleMapper eipAlarmRuleMapper;
//....
@PostConstruct
void init(){
instanceAlarmRuleMapperMap = new ImmutableMap.Builder<TypeEnum, BaseMapper<? extends CommonInsAlarmRule>>()
.put(EcsAlarmRule.class.getName(), ecsAlarmRuleMapper)
.put(EipAlarmRule.class.getName(), eipAlarmRuleMapper)
.put(RdsAlarmRule.class.getName(), rdsAlarmRuleMapper)
// .....
.build();
}
为了更好地利用MybatisPlus的LambdaQueryWrapper静态字段缓存进行代码中相关SQL的静态检查,需要为CommonInsAlarmRule创建一个无需对应任何数据库表的形式化mapper:
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.springframework.stereotype.Repository;
@Repository
public interface AbstractInsMapper extends BaseMapper<CommonInsAlarmRule> {
}
最后,在ConfigUtil中封装增删查改的常用方法:
public static <T extends CommonInsAlarmRule> BaseMapper<T> getInsAlarmRuleMapperByInsType(TypeEnum productType) {
Class<T> clazz = (Class<T>) insClassMap.get(productType);
return (BaseMapper<T>) instanceAlarmRuleMapperMap.get(clazz.getName());
}
public static List<CommonInsAlarmRule> selectList(CloudProductTypeEnum productType, LambdaQueryWrapper<CommonInsAlarmRule> wrapper) {
return getInsAlarmRuleMapperByInsType(productType).selectList(wrapper);
}
public static CommonInsAlarmRule selectOne(CloudProductTypeEnum productType, LambdaQueryWrapper<CommonInsAlarmRule> wrapper) {
return getInsAlarmRuleMapperByInsType(productType).selectOne(wrapper);
}
public int insertOne(CloudProductTypeEnum productType, CommonInsAlarmRule rule){
return getInsAlarmRuleMapperByInsType(productType).insert(rule);
}
举个例子,下图是一段根据实例ID及产品类型查询指定实例的配置阈值的代码,可以明显看出,这段代码的信息熵会随着产品数量的增加而逐渐升高:

使用统一封装之后的接口来替代原先的这段代码,只需三行,且可在后续迭代中保持信息密度不变: