06-EaEngine:549行的调度器
EaEngine 是元数据引擎的总调度------初始化链、缓存解析、六种API入口、事务的success-flag模式、postProcess加密脱敏全在这一个类里。这篇按"启动→查询→更新→异常"的顺序把549行拆完。
文章目录
- 06-EaEngine:549行的调度器
-
- 一、它持有的八个协作者
- 二、init():六步启动链
- 三、describe():手拼JSON的元数据输出
- 四、query():两条通道的分岔
- 五、resolveSqlMeta():缓存miss的synchronized双检
- 六、success-flag事务模式
- 七、executeUpdate():_t参数分发
- [八、resultSetToList 与 postProcess](#八、resultSetToList 与 postProcess)
源码:
browise-metadata/src/main/java/com/browise/ea/core/engine/EaEngine.java
一、它持有的八个协作者
java
public class EaEngine implements EaLifecycle {
private EaConfig config; // 配置(密钥/阈值/自定义策略)
private EaConnectionProvider provider;// 连接提供者(Spring DataSource桥接)
private EaMetadataCache cache; // 元数据缓存(ConcurrentHashMap)
private MetadataLoader metadataLoader;// 五表装载器
private SqlBuilder sqlBuilder; // SQL生成器(持有Dialect)
private Dialect dialect; // 数据库方言(探测后注入)
private SlowSqlMonitor slowSqlMonitor;// 慢SQL监控
private EncryptStrategy encryptStrategy; // SM4(可空)
private SignStrategy signStrategy; // SM2(可空)
}
EaEngine 自己不拼SQL、不连数据库、不做加密------它是纯粹的调度者,每个动作都委托给专职组件。唯一内联实现的是 toJson/escapeJson/mask 这类不值得抽出去的小工具。
二、init():六步启动链
java
public void init(EaConfig config, EaConnectionProvider provider) {
this.config = config;
this.provider = provider;
this.cache = new EaMetadataCache();
this.metadataLoader = new MetadataLoader();
this.slowSqlMonitor = new SlowSqlMonitor(config);
initEncryptStrategy(); // ①自定义策略优先,否则SM4默认
initSignStrategy(); // ②同上,SM2
DdlExecutor ddlExecutor = new DdlExecutor();
ddlExecutor.executeAutoCreate(provider, config); // ③五表不存在则建
ddlExecutor.executeAutoUpgrade(provider, config); // ④列缺失则补
try (Connection conn = provider.getConnection()) {
DatabaseType dbType = DatabaseType.detect(conn, config.isKingBasePgMode());
this.dialect = dbType.createDialect(); // ⑤探测方言
}
this.sqlBuilder = new SqlBuilder(dialect); // ⑥方言注入SQL生成器
metadataLoader.loadAll(provider, cache); // 五表全量装载
}
三个细节:
顺序不能乱------③④必须在⑤前:DdlExecutor自己按DatabaseType建表,而方言探测需要连接(连接要活着才能detect)。⑥必须在⑤后:SqlBuilder构造时就要方言。
双策略初始化的自定义优先:
java
private void initEncryptStrategy() {
EncryptStrategy custom = config.customEncryptStrategy();
if (custom != null) { this.encryptStrategy = custom; return; } // SPI注入优先
if (config.getEncryptKey() != null) {
this.encryptStrategy = new Sm4EncryptStrategy(key, iv, mode); // 否则默认SM4
}
}
不配密钥 → encryptStrategy为null → postProcess跳过解密。加密能力默认关闭,配置了才生效。
refresh() 是synchronized的------元数据热更新(管理界面改了EA03字段)不能和正在进行的loadAll并发:
java
@Override
public synchronized void refresh() {
metadataLoader.loadAll(provider, cache);
}
三、describe():手拼JSON的元数据输出
describe不拼SQL------它把缓存里的元数据序列化成前端能消费的JSON:
java
public String describe(String eaCode) {
EaSqlMeta sqlMeta = resolveSqlMeta(eaCode);
List<EaFieldMeta> fields = cache.getFieldMetas(eaCode);
List<EaConditionMeta> conditions = cache.getConditionMetas(eaCode);
// StringBuilder逐字段append,输出:
// { "sqlCode":"SYS_USER_s", "title":"用户查询",
// "columns":[ {name,label,dataType,displayType,codeName,
// encrypted,masked,pattern,maskRule...} ],
// "conditions":[ {paramName,label,operator,required...} ] }
}
为什么手拼而不用Jackson? metadata模块刻意不依赖任何JSON库------它是纯JDK实现的引擎,springboot装配层(EaAutoConfiguration)才接触Spring。这样引擎可以脱离Spring独立测试(metadata模块108个单测全部纯JVM跑)。
四、query():两条通道的分岔
java
public String query(String eaCode, Map<String, String> params) {
EaSqlMeta sqlMeta = resolveSqlMeta(eaCode);
List<EaFieldMeta> fields = cache.getFieldMetas(eaCode);
List<EaConditionMeta> conditions = cache.getConditionMetas(eaCode);
Integer minRow = parseIntOrNull(params.get("minRow"));
Integer maxRow = parseIntOrNull(params.get("maxRow"));
boolean paginate = minRow != null && maxRow != null;
SqlBuilder.BuiltSql builtSql;
if (sqlMeta.isHasRawSql()) {
builtSql = sqlBuilder.buildFromRawSql(...); // rawSql通道
} else {
builtSql = sqlBuilder.buildSelect(...); // 元数据通道
}
return executeQuery(builtSql, fields);
}
hasRawSql 是分岔点------复杂SQL整条存EA01.EAE800,走占位符替换通道;标准查询走五表组装通道。两条通道最终都汇入同一个executeQuery。
五、resolveSqlMeta():缓存miss的synchronized双检
java
private EaSqlMeta resolveSqlMeta(String eaCode) {
if (!cache.contains(eaCode)) {
synchronized (this) {
if (!cache.contains(eaCode)) {
metadataLoader.loadAll(provider, cache); // 全量重载
}
}
}
EaSqlMeta meta = cache.getSqlMeta(eaCode);
if (meta == null) {
throw new EaException(EaExceptionType.METADATA_ERROR, "未找到SQL编号: " + eaCode);
}
return meta;
}
启动时loadAll过了,为什么运行时还要查?------运行中通过管理界面新增了SQL定义,缓存里没有。双检锁(double-checked locking)避免并发请求触发多次全量重载。找不到时抛METADATA_ERROR而不是返回null------让上层GlobalExceptionHandler统一转成前端错误。
六、success-flag事务模式
executeQuery/executeUpdate共用同一套事务模板(这段修过bug,BROWISE-STATUS记录了修复过程):
java
private String executeQuery(SqlBuilder.BuiltSql builtSql, List<EaFieldMeta> fields) {
long start = System.currentTimeMillis();
boolean myTrans = !provider.hasTransaction(); // ①外层没事务才自己管
boolean success = false;
if (myTrans) provider.beginTrans();
Connection conn = null;
try {
conn = provider.getConnection();
try (PreparedStatement ps = conn.prepareStatement(builtSql.getSql())) {
bindParams(ps, builtSql.getParams()); // ②占位符按序绑定
try (ResultSet rs = ps.executeQuery()) {
List<Map<String, Object>> rows = resultSetToList(rs, fields);
slowSqlMonitor.check(...); // ③慢SQL计时
success = true; // ④成功标志
return toJson(rows);
}
}
} catch (Exception e) {
throw new EaException(SQL_ERROR, builtSql.getSql(), e);
} finally {
if (myTrans) {
if (success) provider.commit(); // ⑤按标志提交/回滚
else provider.rollback();
} else if (conn != null) {
conn.close(); // ⑥外层事务只还连接
}
}
}
关键在①的判断 :如果调用方(如commonSave多store提交)已开启事务,EA引擎不自己commit------它只执行SQL、还连接,事务边界交给外层。这就是"多store一个事务"的实现基础。
修复前的bug:连接管理没有按myTrans分支------外层事务下引擎把连接close了,导致Spring事务提交时连接已失效。success-flag取代了"catch里rollback"的写法,因为catch块本身抛新异常时容易吞掉原始回滚路径。
七、executeUpdate():_t参数分发
java
public int executeUpdate(String eaCode, Map whereParams, Map valueParams) {
String type = valueParams.get("_t");
if (type == null) type = whereParams.get("_t");
if (type == null) type = sqlMeta.getSqlMode(); // 兜底用元数据定义的模式
if ("2".equals(type)) builtSql = sqlBuilder.buildUpdate(...);
else if ("4".equals(type)) builtSql = sqlBuilder.buildInsert(...);
else if ("3".equals(type)) builtSql = sqlBuilder.buildDelete(...);
else if ("1".equals(type) && sqlMeta.isHasRawSql())
builtSql = sqlBuilder.buildFromRawSql(...);
else if ("0".equals(type))
throw new EaException("综合类型需通过 _t 参数指定操作");
}
_t 从行协议泄漏到了执行接口------前端collect('dirty')收集的行自带 _t=1/3/4,commonSave把行连同_t传到这里,引擎按行状态选DML类型。协议字段一杆子插到了SQL生成层。
八、resultSetToList 与 postProcess
java
while (rs.next()) {
Map<String, Object> row = new LinkedHashMap<>(); // 保序
for (int i = 1; i <= colCount; i++) {
String key = meta.getColumnLabel(i).toLowerCase();
Object value;
if (Types.BLOB == meta.getColumnType(i)) {
// BLOB→UTF-8字符串(政务系统里BLOB存的是文本)
Blob blob = rs.getBlob(i);
value = blob != null ? new String(blob.getBytes(1,(int)blob.length()),"UTF-8") : "";
} else {
value = rs.getObject(i);
if (value != null && "null".equals(value.toString())) value = ""; // Oracle的"null"字符串
}
row.put(key, value);
}
postProcess(row, fields); // 每行立刻后处理
}
postProcess两步:isEncrypted先解密、isMasked再脱敏------先解密再脱敏的顺序不能反(脱敏后的星号解不开)。
✅ 亮点:把549行调度器拆成启动链/两条SQL通道/success-flag事务/_t分发四块,重点讲myTrans判断如何支撑多store单事务、BLOB与Oracle"null"字符串的现实处理。适合写引擎类中间件的参考。扩展方向:第07篇拆SqlBuilder六法、第16篇拆SlowSqlMonitor。