质量是产品的生命线,代码检查是软件开发过程中至关重要的一环,它可以帮助我们发现并纠正潜在的错误,提高软件质量,降低维护成本。
在袋鼠云产品中也存在这个问题,由于离线数据开发人员 SQL 水平不一,导致代码书写混乱、SQL 代码运行问题较多。本文将介绍在离线产品中如何利用 SQL 检查规则规范化 SQL 代码,对代码书写问题进行拦截,便于统一管理,用于预防引入需要治理的问题。
通过本文的介绍,我们希望您能够认识到代码检查的重要性,并了解如何通过最佳实践来提高代码质量和开发效率。
何时进行代码规则检查?
SQL 任务在离线产品界面开发完成之后,点击运行的按钮,会先经过代码规则检查,如果代码规则不满足则会提示到用户具体的原因。
数据资产模块内置了 5 种代码检查规则,用户可以根据需要选择性开启。
开启后在离线项目管理中可以选择使用的代码规则检查项、生效范围和 SQL 任务类型。
在离线 SQL 任务中去运行一条 SQL 前会根据选择的规则先进行代码检查,如果代码检查不通过则会反馈给用户,用户可以根据实际需要判断要不要执行该 SQL。
在数据资产的代码检查时间中可以看到已经触发的检查历史以及相应的统计数据。
如何实现代码检查规则?
在 CodeCheck 包下定义了通用的代码规则检查的接口。
public interface ICheck {
Result codeCheck(String checkContent, String defaultDb, Integer dataSourceType, Long tenantId, SqlParseInfo sqlParseInfo);
CodeCheckType getCheckType();
}
以分区表查询必须带分区规则为例,会先调用 SQLParser 组件进行 SQL 解析,SQLParseInfo 即为 SQL 解析结果,检查时会先判断 SQL 语句是不是查询语句,如果是则判断查询的表是不是分区表,再判断是否有查询条件,最后判断查询条件中是否包含分区字段来判断是否检查通过。
public class CodeCheckImplType01 extends AbstractCheck {
private static final Logger LOGGER = LoggerFactory.getLogger(CodeCheckImplType01.class);
@Autowired
private DataTableColumnThirdService dataTableColumnThirdService;
@Autowired
private DataTableThirdService dataTableThirdService;
@Override
public Result codeCheck(String checkContent, String defaultDb, Integer dataSourceType, Long tenantId, SqlParseInfo sqlParseInfo) {
if (!isQuery(sqlParseInfo.getSqlType())) {
return Result.buildSuccessResult();
}
try {
MetadataSearchParam searchParam = new MetadataSearchParam();
searchParam.setDbName(sqlParseInfo.getMainTable().getDb());
searchParam.setTableName(sqlParseInfo.getMainTable().getName());
searchParam.setDataSourceType(dataSourceType);
searchParam.setTenantId(tenantId);
List<TableDTO> tableDTOS = dataTableThirdService.tableList(searchParam);
// 获取表信息
for (TableDTO tableDTO : tableDTOS) {
List<DataTableColumn> tableColumns = dataTableColumnThirdService.listColumnByTableIds(Lists.newArrayList(tableDTO.getTableId()));
if (CollectionUtils.isEmpty(tableColumns)) {
continue;
}
List<String> partitionColumnNameList = tableColumns.stream()
.filter(Objects::nonNull)
.filter(t -> HavePartitionEnum.have_partition.getPartitionValue().equals(t.getIsPartition()))
.map(DataTableColumn::getColumnName)
.collect(Collectors.toList());
// 非分区表直接返回
if (CollectionUtils.isEmpty(partitionColumnNameList)) {
continue;
}
if (CollectionUtils.isEmpty(sqlParseInfo.getColumnIdentifierList())) {
// 没有查询条件则校验失败
return Result.buildFailedResult(String.format(getCheckType().getCheckResultFormat(), searchParam.getTableName()));
}
List<String> columnList = sqlParseInfo.getColumnIdentifierList().stream()
.filter(c -> StringUtils.equals(c.getDb(), searchParam.getSchemaName()) && StringUtils.equals(c.getTable(), searchParam.getTableName()))
.map(ColumnIdentifier::getColumn).collect(Collectors.toList());
boolean disjoint = Collections.disjoint(partitionColumnNameList, columnList);
if (disjoint) {
return Result.buildFailedResult(String.format(getCheckType().getCheckResultFormat(), searchParam.getTableName()));
}
}
} catch (Exception e) {
// 异常情况先通过
LOGGER.error("code check error, check content: {}, defaultDb: {}, checkType: {}", checkContent, defaultDb, getCheckType().name());
}
return Result.buildSuccessResult();
}
@Override
public CodeCheckType getCheckType() {
return CodeCheckType.TYPE_01;
}
}
如何自定义代码检查规则?
如果内置的代码检查规则不满足客户的使用场景,客户可以通过上传 jar 的方式自定义代码检查规则。
自定义代码检查规则使用 SPI 机制加载用户上传的自定义 jar,并在代码检测时调用 CodeCheck 方法,在资源关闭时调用 close 方法,用户需要将配置文件说明中的 jar 依赖自己的项目中。具体如下:
● 创建一个类实现接口
创建一个类实现接口 com.dtstack.assets.spi.codecheck.ICodeCheckClient 并实现 CodeCheck 和 close 方法,书写相关逻辑代码,如果校验通过需要将 CheckResult 对象中 success 设置为 true,失败时设置 success 字段为 false 并设置校验不通过的理由。
package com.dtstack.assets.spi.codecheck;
import java.util.Map;
public interface ICodeCheckClient {
/**
* 代码检查
*
* @param checkContent 检查内容
* @param extMap 扩展配置
* @return 检查结果
*/
CheckResult codeCheck(String checkContent, Map<String, Object> extMap);
/**
* 释放资源, 调用时需要关闭所使用的资源
*/
void close();
}
· 入参字段解释
– checkContent 为单条 SQL 信息
– extMap 会设置一些平台的属性,包含任务名称、任务类型等
· 出参字段解释
– success 为是否校验通过,必须设置
– checkResult 为校验结果,校验不通过时不能为空
package com.dtstack.demo;
import com.dtstack.assets.spi.codecheck.CheckResult;
import java.util.Map;
public class CodeCheckImpl implements com.dtstack.assets.spi.codecheck.ICodeCheckClient{
@Override
public CheckResult codeCheck(String checkContent, Map<String, Object> extMap) {
// 代码检查相关逻辑
CheckResult checkResult = new CheckResult();
checkResult.setSuccess(false);
checkResult.setCheckResult("校验不通过的理由");
return checkResult;
}
@Override
public void close() {
// 关闭相关资源
}
}
● 在 resource 目录下创建 META-INF/services 目录
● 在 META-INF/services 目录下创建文件
文件名称为 com.dtstack.assets.spi.codecheck.ICodeCheckClient ,文件内容为实现 ICodeCheckClient 接口类的权限定类名。
文件名称和内容示例:
● 打包当前工程并在数据资产页面注册代码校验规则
不符合条件的 jar 会给出提示。
如何加载自定义代码规则对应的 jar ?
我们会为上传的每个规则对应的 jar 初始化一个唯一的自定义 classloader,该 classloader 继承 URLClassLoader 并保证子类加载器优先加载。
在第一次调用时进行加载并缓存对应的 client。
在用户重新上传或者编辑规则后清除旧的 classloader 和加载的 client 并释放资源。