借助CAT提供的全量日志功能,助推部门内部的前后端规范的落地。

0. 目录

  • 1. 前言
  • 2. 优势
  • 3. 实现
  • 4. 效果
  • 5. Links


1. 前言

在用户对于WEB应用的界面样式要求和操作便捷性不断提升的背景下,即使是传统的电子政务软件开发公司都开始了全面的前后端分离开发模式。

将原本由一人完成的功能开发拆解为两个人来完成,专业性带来的优化如果不能有效控制前后端开发人员的沟通方式和渠道,很可能出现效率不增反降的尴尬局面。

本文尝试借助前一篇博客 CAT魔改 - 独立化cat-client 收集到的应用层全量日志来进行审计分析,通过红黑榜的形式助推前后端沟通规范的落地,杜绝"落在纸上的规范"。

2. 优势

为了证明本文的必要性和正确性,这一部分是肯定得有的。

  1. 无需额外人力投入,并显著提升落地成功几率。
    a. 过往我们尝试的"培训",“定期的代码审查"等等人工方式,经常陷入一种运动式的规范推进,缺斤少两式的规范执行,虽然双方都进行了巨大的人力物力投入,但往往效果并不是很明显,而且随着人工操作必然的倦怠期的来临,规范的落地往往无疾而终。
    b. 本方法作为规范执行的有效补充,将能够显著提升规范落地的成功几率,且因为所有的数据都是由程序自动收集汇总报表,节省人力的同时,也杜绝了人工操作可能出现的低级错误,绝对的真实,可靠,没有遗漏。
    c. 规范执行情况可视化。 相较于过往规范执行时候的"只能着眼局部”,本审计方法提供了以报表的形式展示整个应用层面的规范落地情况,方便团队对于规范执行情况的全局了解,对齐各方认知,推动规范的执行。
  2. 统计分析更为准确,减少错报,漏报。
    a. 相较于笔者过往曾经实现过的"基于Mybatis的Plugin机制实现契约执行情况的统计",本审计方法能够更为全面地进行分析,例如有的时候我们可能需要在一次请求中针对数据库同时进行多个CRUD操作的混合,这个时候如果只是基于简单的单次数据库操作进行判断很明显是不准确的。
  3. 更高的扩展性。
    a. 正如前文 CAT魔改 - 独立化cat-client 中提到的,因为此扩展功能完全由我们自己实现,因此即使是只针对这项审计功能本身,我们也能提供诸如"多个小组规范执行情况的对比统计展示"等等一系列举措。

3. 实现

废了上面那么多话,接下来进入本文的核心部分。

// 看过本文所引用的《CAT魔改 - 独立化cat-client》,应该都知道本方法的唯一参数哪来的
public void recordContractOfFrontendAndBackend(com.dianping.cat.message.Message message) {
	// 这里是本方法中要注意的第一个点
	// 因为CAT中对于message的消费采用的是经典的"生产者-消费者"模式, 而这里的消费是在单独的线程中,所以对于当前请求是否为了HTTP调用的判断,不能简单地使用诸如 WebUtil.getRequest() !=null 来进行
	if (!isHttpRequest(message)) {
		return;
	}

	final List<SqlTransaction> sqlTransactions = CollectionUtil.newArrayList();
	doRecordContractOfFrontendAndBackend(message, sqlTransactions);

	if (sqlTransactions.isEmpty()) {
		// 没有SQL操作, 直接取消接下来的判断
		return;
	}

	final RequestMethod currentRequestMethod = getCurrentRequestMethod(message);
	final List<String> currentSqlCommandTypes = sqlTransactions.stream().map(SqlTransaction::getSqlCommandType).collect(Collectors.toList());
	// 这一步就是为了兼容"一次HTTP请求中出现多次SQL操作"的情况
	// 当前查询中是否有匹配的规则(只要有一个SQL操作与当前的HTTP Method匹配即可)
	final boolean anyMatch = Contracts.mapping.get(currentRequestMethod).stream()
		  .anyMatch(s -> currentSqlCommandTypes.contains(s.name().toLowerCase()));

	if (anyMatch) {
		return;
	}

	// 记录下违约的情况, 用于之后的报表呈现
	final SqlAndHttpMethodContractViolation contractViolationEntity = new SqlAndHttpMethodContractViolation(
		  getCurrentRequestUrl(message), currentRequestMethod, sqlTransactions.get(0).getMybatisId(), SqlCommandType.valueOf(currentSqlCommandTypes.get(0).toUpperCase()));
	if (!contractViolations.contains(contractViolationEntity)) {
		contractViolations.add(contractViolationEntity);
		FileUtil.appendLines(Collections.singleton(JsonUtil.toJson(contractViolationEntity)),
			  FILE_CONTRACT_VIOLATION_PATH, CharsetUtil.UTF_8);
	}
}

static boolean isHttpRequest(Message message) {
	return (message instanceof Transaction) && (((Transaction)message).getType().equals("URL"));
}

static RequestMethod getCurrentRequestMethod(Message message) {
	final Transaction transaction = ((Transaction)message);
	final String httpUrl = Convert.toStr(transaction.getChildren().get(1).getData()).split("\\s")[0];
	return RequestMethod.valueOf(StringUtil.subAfter(httpUrl, "/", true));
}

static String getCurrentRequestUrl(Message message) {
	final Transaction transaction = ((Transaction)message);
	return Convert.toStr(transaction.getChildren().get(1).getData()).split("\\s")[1];
}

private void doRecordContractOfFrontendAndBackend(Message message, final List<SqlTransaction> sqlTransactions) {
	if (message instanceof Transaction) {
		Transaction transaction = (Transaction) message;
		if (transaction.getType().equalsIgnoreCase("SQL")) {
			sqlTransactions.add(sqlTransaction(transaction));
		} else {
			List<Message> children = transaction.getChildren();
			int len = children.size();

			for (int i = 0; i < len; i++) {
				Message child = children.get(i);
				if (child != null) {
					doRecordContractOfFrontendAndBackend(child, sqlTransactions);
				}
			}
		}
	}
}

private SqlTransaction sqlTransaction(Transaction sqlTransaction) {
	final String mybatisId = sqlTransaction.getName();
	final String sqlCommandType = sqlTransaction.getChildren().get(0).getName();
	//final String databaseType = sqlTransaction.getChildren().get(1).getName();
	return new SqlTransaction(mybatisId, sqlCommandType);
}

// ==========================
@Data
@AllArgsConstructor
private static final class SqlTransaction {
	private String mybatisId;

	private String sqlCommandType;
}

// ========================== 部门内部的前后端契约
// 笔者小组目前为了降低推进难度, 并没有完全遵照restful中的http method对应
//	1. GET ----- SELECT
//	2. POST ---- INSERT / UPDATE / DELETE
class Contracts {
	static final Map<RequestMethod, EnumSet<SqlCommandType>> mapping;
	static {
		mapping = MapUtil.newHashMap();
		mapping.put(RequestMethod.GET, EnumSet.of(SqlCommandType.SELECT));
		//mapping.put(RequestMethod.PUT, EnumSet.of(SqlCommandType.UPDATE));
		//mapping.put(RequestMethod.DELETE, EnumSet.of(SqlCommandType.DELETE));
		mapping.put(RequestMethod.POST, EnumSet.of(SqlCommandType.INSERT, SqlCommandType.UPDATE, SqlCommandType.DELETE));
	}
}

// ==========================
@Getter
class SqlAndHttpMethodContractViolation {
	private String httpUrl;

	private RequestMethod httpMethod;

	private String mybatisId;

	private SqlCommandType sqlCommandType;

	private String tip;

	public SqlAndHttpMethodContractViolation(String httpUrl, RequestMethod httpMethod, String mybatisId,
	      SqlCommandType sqlCommandType) {
		super();
		this.httpUrl = httpUrl;
		this.httpMethod = httpMethod;
		this.mybatisId = mybatisId;
		this.sqlCommandType = sqlCommandType;

		tip = httpMethod.equals(RequestMethod.GET) ? "INSERT/UPDATE/DELETA 数据库操作语句请使用 POST 请求"
		      : "SELECT 数据库操作语句请使用 GET 请求";
	}

	@Override
	public boolean equals(Object other) {
		if (this == other) {
			return true;
		}

		if (other == null || other.getClass() != this.getClass()) {
			return false;
		}

		SqlAndHttpMethodContractViolation that = (SqlAndHttpMethodContractViolation) other;

		// 比较自身的特有属性;父类的就交给父类.
		if (this.httpUrl.equalsIgnoreCase(that.httpUrl)) {
			return true;
		}

		//其它的比较交给父类
		return super.equals(other);
	}

	@Override
	public int hashCode() {
		return sqlCommandType.hashCode();
	}
}

4. 效果

通过访问约定的审计页面,我们就能查看所有被执行过的WEB请求是否符合我们的前后端契约,为规范的进一步推进提供足够的基础信息。

前端加后端java 前端加后端审计模式_前后端分离

5. Links

  1. CAT魔改 - 独立化cat-client