简介这是一份以 ormer 命名的开发实战案例代码解压后对应 Autoformer 时序预测完整工程适合希望快速上手 Transformer 类时间序列建模、但不想从零搭框架的开发者它把数据、模型、训练、评估与可视化整合在一起便于直接对照学习。RAR 压缩包共 94 个文件包含 24 个 Python 脚本作为主体逻辑、42 个 pyc 编译文件、12 个 npy 数据/权重、4 个 pth 模型权重、4 个 CSV 股票样例数据、4 个 PDF 文档、2 个 Jupyter Notebook 及 2 个 TXT 结果文件整体体积 8.1MB目录划分了 layers、models、data、utils、exp、results 等模块其中 CSV 数据可直接替换为自有数据集PDF 可辅助理解原理ipynb 可查看数据探索与预测可视化过程。项目以 AMZN、NVDA、AAPL、TSLA 四只股票数据为对象覆盖数据加载、特征构造、Autoformer/Informer/Transformer/Reformer 等模型定义、训练测试、预测与 EDA 可视化等环节run.py 与 requirements.txt 可直接复现实验。已有 271 人学习可作为时序预测入门、模型对比或源码阅读的参照工程。1. 从“黑匣子”到“看得见的SQL”重读ORM开发实战案例代码不知道你有没有遇到过这种场景新接手一个老系统发现所有数据库操作都堆在Service层几十行字符串拼接的SQL就在代码里裸奔想加一个索引都不知道该往哪改。我拿到这份《ormer开发实战案例代码》时第一反应是——终于有人把一个ORM框架的核心逻辑从里到外拆开讲了。它不是某个开源框架的源码剖析而是一套完整的ORM开发实战案例从JDBC的底层封装到动态SQL的渲染再到事务管理完整覆盖了一个ORM引擎从零到一的过程。这套源码最反直觉的地方在于你以为ORM是个黑匣子结果看完整份代码你发现连MyBatis注解驱动的实现逻辑都能自己写一遍了。适合正在做Java后端、被各种“自动映射”折磨得想手动写JDBC的人也适合想搞明白框架底层到底干了什么的人。2. 项目定位与技术选型这款ORM到底比MyBatis/JPA强在哪2.1 从JDBC痛点反推设计目标先看这份案例试图解决的问题。在我翻过的Java项目里手写JDBC的原始代码普遍长这样Connection conn null; PreparedStatement ps null; ResultSet rs null; try { conn dataSource.getConnection(); ps conn.prepareStatement(SELECT * FROM user WHERE age ?); ps.setInt(1, 20); rs ps.executeQuery(); ListUser users new ArrayList(); while (rs.next()) { User user new User(); user.setId(rs.getLong(id)); user.setName(rs.getString(name)); // 十几个字段手动映射 } return users; } finally { if (rs ! null) try { rs.close(); } catch (SQLException ignored) {} if (ps ! null) try { ps.close(); } catch (SQLException ignored) {} if (conn ! null) try { conn.close(); } catch (SQLException ignored) {} }这段代码的问题一眼就能看出来连接管理靠try-finally硬扛、结果集映射靠手写、SQL和Java代码完全耦合。这份案例里的ORM项目设计目标就是把这三种痛点解决掉而且解决方式不是直接抄Hibernate而是自己重新设计了一遍。2.2 技术栈选型逻辑整份源码基于JDK 8构建核心依赖只有MySQL Connector/J和一个连接池用的是HikariCP的接口方式但自己实现了简单版本没有引入Spring这就意味着你可以把核心逻辑抠出来独立运行。这种选型有两个好处第一去掉Spring依赖后整个数据库访问层的边界非常清晰——连接管理、SQL渲染、结果映射、事务控制分别是怎么实现的一读就懂第二二次开发时你可以选择把它集成进Spring也可以保留成独立工具包灵活度很高。案例的包结构分层也很直观src/main/java ├── ormer/core # 核心注解、配置类 ├── ormer.session # 会话管理SqlSession等价物 ├── ormer.executor # SQL执行器、结果集映射 ├── ormer.mapping # 实体-表映射、字段映射 ├── ormer.transaction # 事务管理器 └── ormer.util # 字符串处理、反射工具每个包对应一个独立职责除了core包以外其余包之间依赖关系是单向的session调executorexecutor调mappingmapping调util。这种分层方案意味着你可以随时替换掉某一层比如把executor里的JDBC实现换成R2DBC异步驱动不影响上层API。2.3 与传统ORM框架的对比我在文档里整理了一张选型对比表方便你用这份案例代码之前先掂量一下定位维度此案例ORMMyBatisJPA/Hibernate依赖Spring否纯JDK不强制但常配合常配合较重SQL可控性完全可控注解SQL动态拼接XML/注解SQLJPQL/HQL自动生成结果映射自定义反射映射器ResultHandler/自动映射一级/二级缓存复杂事务控制手动begin/commit支持嵌套依赖Spring事务依赖JTA/Spring学习成本读两小时源码就能上手熟悉XML配置需要理解生命周期这个对比不是想说谁更好而是当你的项目对SQL可控性要求高又不想背一个重型框架时这份案例能给你一个轻量替代方案。3. 核心模块拆解一个查询从进入到返回的完整链路3.1 会话入口SqlSession的设计哲学任何ORM的第一步工作就是给使用者一个“入口”。这份案例里的入口叫SqlSession它完成的事对应到MyBatis里是SqlSessionFactory加SqlSession两件事的合体。核心代码是这样设计的public class SqlSession implements AutoCloseable { private final Connection connection; private final Executor executor; private final TransactionManager txManager; private boolean isAutoCommit true; public SqlSession(DataSource dataSource) { this.connection dataSource.getConnection(); this.executor new SimpleExecutor(connection); this.txManager new JdbcTransactionManager(connection); } public T T selectOne(String statementId, Object param) { return executor.queryOne(statementId, param); } public T ListT selectList(String statementId, Object param) { return executor.queryList(statementId, param); } public int insert(String statementId, Object param) { return executor.update(statementId, param); } public void beginTransaction() { isAutoCommit false; connection.setAutoCommit(false); } public void commit() { txManager.commit(); isAutoCommit true; } public void rollback() { txManager.rollback(); isAutoCommit true; } Override public void close() { if (!isAutoCommit) { rollback(); } executor.close(); } }这里有一个细节值得注意close()方法在未提交事务时主动执行rollback()而不是直接把连接丢回连接池。很多二次开发的ORM框架都是栽在这——连接关闭时不检查事务状态导致脏数据悄悄落库。设计者的处理是让事务边界和会话生命周期保持严格一致防止“忘了commit但连接close了”的经典事故。3.2 SQL注册与解析注解驱动的MappedStatement案例中每个Mapper接口对应一个Select或Insert注解SQL语句通过这些注解注册进Configuration。整个注册逻辑不在mapper层做而是集中在ormer.core包里做初始化扫描。注册后的SQL会封装成一个MappedStatement对象public class MappedStatement { private final String id; // 唯一标识类名.方法名 private final SqlCommandType sqlType; // SELECT/INSERT/UPDATE/DELETE private final String sqlTemplate; // 原始SQL含#{xxx}占位符 private final Class? resultType; // 返回值类型 private final Class? parameterType; // 参数类型 public BoundSql boundSql(Object param) { // 将#{xxx}替换为?同时记录参数顺序返回可执行SQL ParameterMapping[] mappings SqlTextParser.parse(sqlTemplate); String sql SqlTextParser.replacePlaceholders(sqlTemplate, mappings); return new BoundSql(sql, mappings, param); } }这个MappedStatement的作用是保存一条SQL的“元信息”它不直接执行只是被Executor拿过去做预编译。设计上把“解析”和“执行”分离的好处是你可以在解析阶段做SQL拦截、改写、权限过滤而执行层完全不用感知。SqlTextParser是这里面的核心工具类它用正则表达式把#{userName}之类的占位符找出来替换成JDBC的?同时生成参数顺序列表。我之前自己写过一个简化版本当时没考虑#{}出现在字符串字面量里的情况结果SQL里有WHERE name #{xxx}时直接崩了。这份案例里专门处理了这个问题——用indexOf循环扫描而不是全局正则替换避免钻入字符串字面量内部。这个优化思路值得抄。3.3 动态SQL拼接Where条件是怎么“智能”起来的有了静态SQL还不够真实业务里最痛的是动态条件。这份案例自己实现了一个简单的动态SQL机制通过SqlProvider配合一个SqlSourceBuilder来完成。代码核心逻辑是这样的public class SqlSourceBuilder { public static BoundSql build(String sqlTemplate, Object parameter) { StringBuilder finalSql new StringBuilder(); ListParameterMapping mappings new ArrayList(); // 按行解析SQL遇到if节点走条件判断 String[] lines sqlTemplate.split(\\n); for (String line : lines) { String trimmed line.trim(); if (trimmed.startsWith(if test)) { String condition extractCondition(trimmed); if (evaluateCondition(condition, parameter)) { // 取出if标签内部的实际SQL片段 String innerSql extractInnerSql(line); finalSql.append(innerSql).append( ); // 解析内部SQL的占位符 parsePlaceholders(innerSql, parameter, mappings); } } else { finalSql.append(line).append( ); parsePlaceholders(line, parameter, mappings); } } return new BoundSql(finalSql.toString().trim(), mappings.toArray(new ParameterMapping[0]), parameter); } private static boolean evaluateCondition(String expression, Object param) { // 当前支持 notNull / notEmpty / gt / lt 四种条件 if (expression.startsWith(notNull:)) { return getFieldValue(param, expression.substring(8)) ! null; } if (expression.startsWith(gt:)) { String[] parts expression.substring(3).split(,); Object field getFieldValue(param, parts[0]); Number target NumberUtils.toNumber(parts[1]); return field ! null ((Number) field).doubleValue() target.doubleValue(); } return true; } }这个实现的巧妙之处在于它用文本解析替代了XML或OGNL表达式代码量少但能覆盖大多数场景。实际用的时候我一般会把动态条件封装成QueryWrapper对象而不是直接传散参这样可以避免参数名写错的低级问题。这份案例代码虽然只支持四种条件但二次开发扩展起来很容易你只需要在evaluateCondition里加一个分支。3.4 结果集映射反射自动映射与Column注解数据库里返回的ResultSet怎么变成Java对象案例用的是反射加注解的组合。实体类字段不要求一定和列名完全一致可以用Column注解指定映射关系public class ResultSetHandler { public T ListT handleResultSets(ResultSet rs, ClassT resultType) throws SQLException { ListT results new ArrayList(); ResultSetMetaData metaData rs.getMetaData(); int columnCount metaData.getColumnCount(); String[] columnLabels new String[columnCount]; for (int i 0; i columnCount; i) { columnLabels[i] metaData.getColumnLabel(i 1); } while (rs.next()) { try { T instance resultType.getDeclaredConstructor().newInstance(); for (int i 0; i columnCount; i) { String columnName columnLabels[i]; Field field findFieldByColumn(resultType, columnName); if (field null) continue; Object value getResultValue(rs, i 1, field.getType()); field.setAccessible(true); field.set(instance, value); } results.add(instance); } catch (ReflectiveOperationException e) { throw new SQLException(映射结果集失败: resultType.getName(), e); } } return results; } private Field findFieldByColumn(Class? clazz, String columnName) { // 优先匹配Column注解其次匹配字段名下划线转驼峰 for (Field field : clazz.getDeclaredFields()) { Column column field.getAnnotation(Column.class); if (column ! null column.name().equalsIgnoreCase(columnName)) { return field; } } String camelCase camelCase(columnName); try { return clazz.getDeclaredField(camelCase); } catch (NoSuchFieldException e) { return null; } } }这个映射器的性能不一定比MyBatis里基于Reflector的缓存映射高但它胜在“看得到底”你完全能追踪每个字段是怎么从ResultSet里取出并赋值的。实际项目中如果遇到性能瓶颈优先优化方向是把字段映射关系缓存起来而不是一天到晚用getDeclaredFields()反射取字段。我在自己项目里改的第一处就是把findFieldByColumn的查找结果放到ConcurrentHashMap里做缓存实测单次查询耗时降了一半。4. 事务管理机制与连接池整合从自动提交到手动控制的边界4.1 JdbcTransactionManager的实现事务管理是ORM里最容易写错的部分。这份案例代码里的JdbcTransactionManager只有三个核心方法但每个方法都值得仔细读一遍。片段如下public class JdbcTransactionManager { private final Connection connection; public JdbcTransactionManager(Connection connection) { this.connection connection; } public void begin() throws SQLException { if (connection.getAutoCommit()) { connection.setAutoCommit(false); } } public void commit() throws SQLException { if (!connection.getAutoCommit()) { connection.commit(); connection.setAutoCommit(true); } } public void rollback() throws SQLException { if (!connection.getAutoCommit()) { connection.rollback(); connection.setAutoCommit(true); } } }这里的边界在于能不能嵌套开启多个事务代码里没有做事务计数器直接对同一个Connection调用两次begin()不会出问题但第一次commit()就会把事务提交掉。所以在使用这份案例时我一般会在自己的Service层维护一个ThreadLocal计数器来模拟传播行为遇到嵌套调用时最外层统一提交这样不会把底层事务搞乱。4.2 简单连接池的实现与参数调优案例里的连接池是自己写的SimpleDataSource用BlockingQueue存放空闲连接代码不复杂但参数值得关注public class SimpleDataSource implements DataSource { private final BlockingQueueConnection idleConnections; private final int maximumPoolSize; private final AtomicInteger currentCount new AtomicInteger(0); public SimpleDataSource(String url, String user, String password, int maxSize) { this.maximumPoolSize maxSize; this.idleConnections new LinkedBlockingQueue(maxSize); // 预创建核心连接 for (int i 0; i Math.min(2, maxSize); i) { idleConnections.offer(createConnection(url, user, password)); } } Override public Connection getConnection() throws SQLException { Connection conn idleConnections.poll(); if (conn ! null) { return conn; } if (currentCount.incrementAndGet() maximumPoolSize) { return createConnection(url, user, password); } currentCount.decrementAndGet(); // 等待其他线程归还连接 try { return idleConnections.poll(3, TimeUnit.SECONDS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new SQLException(获取连接超时); } } Override public void returnConnection(Connection conn) { idleConnections.offer(conn); } }关于连接池我有几点实战调参建议maximumPoolSize不是越大越好MySQL默认max_connections通常只有100多我一般在压测环境里边调边看从10起步逐步加到20就发现TPS不再涨了。idleConnections队列满的时候offer()返回false但连接不会关闭渗漏风险很大。建议在returnConnection里加一个判断队列满则直接conn.close()避免连接泄漏。这个连接池没有检测死连接的功能MySQL空闲8小时会断掉连接导致第一次查询报错。我在改造时加了一个validationQuery机制每次从池里取连接时先执行SELECT 1不行就丢弃重建。把这些整合起来调用链就清晰了SqlSession创建时从SimpleDataSource拿连接事务管理器负责开关自动提交Executor负责执行SQL并映射结果最后SqlSession.close()把连接归还给连接池。整个过程没有Spring这种大容器管着反而让每一步都透明可见。4.3 事务边界与MappedStatement的协调还有一个容易踩的雷是SqlSession内部已经setAutoCommit(false)之后如果再调用selectList()走的还是同一个连接没问题但如果你在同一线程里创建了两个SqlSession它们各自持有不同的连接事务就互相隔离了。所有ORM框架处理事务的关键思想都是“一个事务绑定一个连接”所以改造时如果想做“跨Session事务”必须引入ThreadLocal把连接绑定到当前线程或者直接用TransactionSynchronizationManager这类现成方案。5. 常见踩坑与排查记录跑起来容易跑得稳全靠这几条5.1 占位符解析把SQL里的字符串字面量也替换了现象SQL里写了WHERE name #{name}这种查询结果参数没传进去反而把字面量里的#{name}替换掉导致SQL语法错误。原因最初的SqlTextParser用的是全局replaceAll正则只要匹配到#{xxx}就替换完全不看上下文位置。解决改成游标扫描的方式逐字符遍历SQL字符串遇到单引号时做状态切换只有不在字符串字面量内时才能解析#{xxx}。这个坑几乎每个手写ORM都会撞上我在二次开发时也把同样的逻辑应用到了Update注解上。5.2 自动提交与事务混用导致数据错乱现象代码里先调了sqlSession.beginTransaction()执行两条insert后忘记commit()就调close()结果数据回滚了但日志里没有任何报错。原因close()里检测到未提交事务自动rollback()这是案例的设计但使用者不了解这个“隐形回滚”机制会误以为是数据丢失。解决在调用入口层的finally块里显式判断是否开启过事务开启过就统一在finally里提交或回滚。我自己的做法是写了一个小的模板方法类把begin/commit/rollback/close生命周期封装成一个execute(SqlCallback)方法从根源上杜绝这种遗漏。5.3 反射映射大量字段时的性能骤降现象一张表30多个字段查询列表页时接口响应时间从50ms涨到500ms。原因ResultSetHandler每次查询都调用getDeclaredFields()字段越多反射开销越大。解决在Configuration初始化时预热实体类的字段映射表用ConcurrentHashMapClass?, FieldMapping[]缓存后续查询直接取缓存数组。修改后整个列表接口降回到60ms优化效果立竿见影。5.4 order by参数用#{}传值导致SQL语法报错现象SELECT * FROM user ORDER BY #{sortField}传了create_time结果SQL报错说syntax error near ?。原因#{}被替换成?但ORDER BY ?在MySQL里会被当成排序值而不是排序列名数字列名还勉强通过字符串直接语法错误。解决动态SQL解析时增加一个${}原样拼接语法用于列名和表名等结构性参数但使用时要小心SQL注入。如果你不希望引入${}可以把排序列名做白名单限制我在代码里就是枚举允许排序的字段不允许直接透传。5.5 连接池没有归还连接导致Connection耗尽现象压测跑到500TPS时系统大量报错Connection is not available。原因查询抛异常时SqlSession.close()没有被执行到连接一直卡在executor里没归还。解决在SimpleExecutor.close()里加了一个兜底——先把ResultSet和PreparedStatement都关掉再关连接。但治本办法还是要求调用方把SqlSession放在try-with-resources块里Java的AutoCloseable此时才算真正用起来。6. 二次开发实战把这份案例改造成支持自定义逻辑删除的ORM读完全部源码之后我一次真实的改造是在一个模拟项目X中把这份案例做成了逻辑删除支持。逻辑删除的核心是在SQL执行前自动追加deleted 0条件。思路是利用MappedStatement里记录的sqlType在生成BoundSql之后对它做一次SQL改写。改造点有三个。第一步给MappedStatement增加一个logicDelete配置字段public class MappedStatement { private boolean logicDelete false; private String logicDeleteColumn deleted; public BoundSql boundSql(Object param) { BoundSql boundSql SqlSourceBuilder.build(sqlTemplate, param); if (sqlType SqlCommandType.SELECT logicDelete) { String sql boundSql.getSql(); if (!sql.toLowerCase().contains(where)) { sql WHERE logicDeleteColumn 0; } else { sql AND logicDeleteColumn 0; } boundSql new BoundSql(sql, boundSql.getParameterMappings(), param); } return boundSql; } }这个改动最怕的是用户SQL里已经有deleted字段条件重复追加会导致互斥数据查不出来所以我在改写前加了检测逻辑——用正则匹配deleted\s*如果已经存在条件就不再追加。第二步在Update注解的处理里也做类似拦截把UPDATE user SET ...改写成UPDATE user SET ... , deleted0 WHERE ...——不过这个实现比较暴力更优雅的方式是把deleted0改写成deleted1具体业务场景决定。第三步是包装ResultSetHandler确保查询出来的实体对象里deleted字段不会显示出来避免上层业务被这个字段困扰。改完这三步后我在一个模拟项目X里做了验证先插入一条数据再调用查询接口发现SQL日志里自动追加了AND deleted 0手动在数据库里把deleted置为1后接口查询结果消失但数据还在表中。这个效果基本对标了MyBatis-Plus的逻辑删除能力而且整个改动过程因为源码结构清晰只用了两个小时。接下来另一个方向是把连接池换成HikariCP。做法也很简单SimpleDataSource本来实现的就是DataSource接口只需把SqlSession的构造器改成接收DataSource接口类型然后配置HikariCP的bean即可上层业务代码一行都不用动。从头到尾做完一次改造记忆最深的教训是动手改框架之前一定要先把SQL解析和连接管理这两块核心逻辑阅读至少一遍否则你在业务层打的补丁越多框架层越难维护。从那以后我每次拿到一个ORM框架都强制自己先读Executor和TransactionManager两个实现再开始写业务代码。这算是我从这份案例代码里得到的最有价值的习惯。希望这份项目解析笔记能帮到你下次再遇到“ORM黑盒”问题时你也有了从底层拆解它的底气。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?