1. 为什么在EOS7.5里还要手写接口
第一次接触EOS7.5这套低代码开发平台的人,通常会有个疑问:平台都提供了可视化拖拽、逻辑流编排、页面建模这些能力了,为什么还要手写接口?直接用平台内置的构件和逻辑流不就行了吗?
这个问题我在实际项目里被问过不下十次。答案其实不复杂——低代码平台解决的是"标准化场景的快速交付",但真实业务里总有一部分逻辑是标准化构件覆盖不到的。比如对接第三方系统的私有协议、处理复杂的批量数据校验、实现特定的加密签名算法、或者需要极致性能的查询场景。这些情况下,手写接口就是必要的补充手段。
EOS7.5作为一款成熟的国产低代码开发平台,它的定位是"构件化+可视化+可扩展"。平台本身提供了大量的内置构件,覆盖了增删改查、流程流转、权限控制等常见需求。但它的扩展能力同样重要——允许开发者用Java手写接口,然后通过配置的方式挂载到平台中,供逻辑流或页面调用。这就形成了一个"可视化为主、手写为辅"的开发模式。
我这次要分享的,就是在一个实际项目中,如何在EOS7.5里手写接口,并且按照经典的三层架构(Controller-Service-DAO)来组织代码。三层架构不是什么新概念,但放在低代码平台的语境下,它有了新的意义:它让手写代码和平台可视化部分之间有了清晰的边界,后续维护和交接都不会乱。
这篇文章适合两类人看:一类是刚接触EOS7.5、需要做平台扩展开发的工程师;另一类是有Java基础但不确定怎么在低代码平台里组织代码结构的开发者。我会从架构设计思路讲到具体实现步骤,再到踩过的坑和排查技巧,尽量把每个环节的"为什么"说清楚。
2. 三层架构在EOS7.5中的落地思路
2.1 三层架构的核心逻辑回顾
三层架构这个概念,做Java Web开发的人应该都不陌生。它把应用分成表现层(Controller)、业务逻辑层(Service)和数据访问层(DAO)。Controller负责接收请求、参数校验、组装响应;Service负责业务逻辑编排、事务控制;DAO负责和数据库打交道,执行SQL或调用ORM框架。
这个分法的核心价值在于"关注点分离"。Controller不需要知道数据从哪来,Service不需要关心请求是HTTP还是RPC,DAO不需要理解业务规则。每一层只做自己该做的事,层与层之间通过接口通信。
在传统的Spring Boot项目里,这套结构已经很成熟了。但在EOS7.5这样的低代码平台里,情况有些不同。平台本身有自己的构件体系、逻辑流引擎、页面渲染机制。手写接口不是独立运行的,而是要被平台"托管"的。所以三层架构的落地方式需要做一些适配。
2.2 EOS7.5对手写接口的托管机制
EOS7.5的手写接口本质上是一段Java代码,它需要实现平台规定的接口规范,然后通过配置文件注册到平台中。平台在运行时会加载这些接口,当逻辑流或页面调用时,平台负责把请求参数传递进来,把返回值传递出去。
这意味着几个关键点:第一,手写接口的入口方法签名是平台规定的,不能随意改;第二,接口的注册和配置是通过XML或注解完成的,不是靠Spring的自动扫描;第三,事务管理、日志记录这些横切关注点,需要开发者自己处理,平台不会自动帮你做。
理解了这些,三层架构的适配思路就清晰了:Controller层对应平台接口的实现类,负责适配平台的调用规范;Service层是纯Java的业务逻辑,不依赖平台API;DAO层可以用平台提供的数据访问构件,也可以用原生的JDBC或MyBatis。这样分层的好处是,Service和DAO层的代码可以脱离平台独立测试,Controller层只做薄薄的适配。
2.3 为什么不用平台内置构件替代
有人可能会问:EOS7.5本身有数据访问构件、有逻辑流编排,为什么还要手写DAO和Service?
这个问题要分场景回答。如果是简单的单表增删改查,用平台构件确实更快,拖拽配置就能完成,没必要手写。但以下几种情况,手写更合适:
- 复杂查询:涉及多表关联、动态条件拼接、子查询嵌套的场景,用构件配置会非常繁琐,SQL直接写更清晰。
- 批量处理:需要循环处理大量数据、做批量校验和转换的场景,逻辑流编排的性能不如手写代码。
- 第三方对接:需要调用外部系统的API、处理私有协议、做数据格式转换的场景,手写接口更灵活。
- 性能敏感:对响应时间有严格要求的查询,手写SQL可以做针对性的索引优化和缓存策略。
所以我的原则是:能用构件解决的用构件,构件解决不了的用手写,手写的时候按三层架构组织代码。这样既享受了低代码平台的效率,又保留了应对复杂场景的能力。
3. 手写接口的核心细节与实操要点
3.1 接口定义与注册的关键步骤
在EOS7.5里手写一个接口,第一步是定义接口类。这个类需要实现平台规定的接口,通常是com.eos.system.logicflow.ILogicFlow或者类似的接口(具体包名根据版本可能不同)。实现类需要重写execute方法,这个方法接收一个上下文对象,返回执行结果。
public class QueryOrderListFlow implements ILogicFlow { @Override public Object execute(LogicFlowContext context) throws Exception { // 从上下文获取参数 String orderNo = (String) context.getInput("orderNo"); String startDate = (String) context.getInput("startDate"); // 调用Service层 OrderService orderService = new OrderServiceImpl(); List<OrderDTO> result = orderService.queryOrders(orderNo, startDate); // 返回结果 return result; } }这段代码看起来简单,但有几个细节需要注意。第一,context.getInput()返回的是Object类型,需要做类型转换,转换前最好做null判断和类型校验。第二,Service层的实例化方式,在平台环境下可以用工厂模式或者简单的new,但如果Service有依赖注入需求,需要自己实现一个简单的容器。第三,异常处理,execute方法抛出的异常会被平台捕获并记录,但返回给调用方的错误信息需要自己组装。
接口定义好之后,需要在平台的配置文件中注册。EOS7.5通常使用logicflow-config.xml或类似的配置文件,在里面声明接口的ID、实现类、输入参数、输出参数等信息。注册完成后,逻辑流设计器里就能看到这个接口,可以像使用内置构件一样拖拽使用。
3.2 Service层的业务逻辑组织
Service层是整个三层架构的核心,业务逻辑都在这里。在EOS7.5的手写接口场景下,Service层有几个设计要点。
首先是接口与实现分离。定义OrderService接口,然后写OrderServiceImpl实现类。这样做的好处是,Controller层依赖接口而不是实现,后续如果业务逻辑有变化,只需要换实现类,Controller层不用改。同时,单元测试的时候可以用Mock实现替换真实实现。
public interface OrderService { List<OrderDTO> queryOrders(String orderNo, String startDate) throws ServiceException; void batchUpdateStatus(List<String> orderIds, String status) throws ServiceException; } public class OrderServiceImpl implements OrderService { private OrderDAO orderDAO = new OrderDAOImpl(); @Override public List<OrderDTO> queryOrders(String orderNo, String startDate) throws ServiceException { // 参数校验 if (startDate != null && !isValidDate(startDate)) { throw new ServiceException("日期格式不正确"); } // 调用DAO List<OrderDO> doList = orderDAO.selectByCondition(orderNo, startDate); // DO转DTO return convertToDTO(doList); } }Service层的另一个要点是事务控制。在EOS7.5里,平台的事务管理主要针对构件操作,手写接口的事务需要自己处理。我的做法是在Service层的方法入口开启事务,方法结束时提交,异常时回滚。可以用编程式事务,也可以用AOP。如果项目里手写接口不多,编程式事务更简单直接。
public void batchUpdateStatus(List<String> orderIds, String status) throws ServiceException { Connection conn = null; try { conn = DataSourceUtil.getConnection(); conn.setAutoCommit(false); for (String orderId : orderIds) { orderDAO.updateStatus(conn, orderId, status); } conn.commit(); } catch (Exception e) { if (conn != null) { try { conn.rollback(); } catch (SQLException ex) { /* 记录日志 */ } } throw new ServiceException("批量更新失败", e); } finally { if (conn != null) { try { conn.close(); } catch (SQLException e) { /* 记录日志 */ } } } }注意:事务的粒度要控制好。不要在一个事务里做太多事情,否则锁持有时间过长,会影响并发性能。批量操作时,可以考虑分批提交,比如每500条提交一次。
3.3 DAO层的数据访问实现
DAO层负责和数据库打交道。在EOS7.5里,可以用平台提供的数据源,也可以用自己配置的连接池。我倾向于用平台的数据源,因为这样数据库连接的管理统一在平台层面,不需要额外维护连接池配置。
DAO层的实现方式有两种:一种是用原生JDBC,另一种是用MyBatis或Hibernate这样的ORM框架。在低代码平台的手写接口场景下,我推荐用原生JDBC或者轻量级的MyBatis。原因是:ORM框架虽然方便,但它的配置和平台本身的配置容易冲突,而且ORM的懒加载、缓存等特性在平台环境下可能表现异常。原生JDBC虽然代码量大一些,但可控性强,出问题容易排查。
public class OrderDAOImpl implements OrderDAO { @Override public List<OrderDO> selectByCondition(String orderNo, String startDate) throws DAOException { StringBuilder sql = new StringBuilder("SELECT * FROM t_order WHERE 1=1 "); List<Object> params = new ArrayList<>(); if (orderNo != null && !orderNo.isEmpty()) { sql.append("AND order_no = ? "); params.add(orderNo); } if (startDate != null && !startDate.isEmpty()) { sql.append("AND create_date >= ? "); params.add(startDate); } sql.append("ORDER BY create_date DESC"); Connection conn = null; PreparedStatement ps = null; ResultSet rs = null; try { conn = DataSourceUtil.getConnection(); ps = conn.prepareStatement(sql.toString()); for (int i = 0; i < params.size(); i++) { ps.setObject(i + 1, params.get(i)); } rs = ps.executeQuery(); List<OrderDO> result = new ArrayList<>(); while (rs.next()) { OrderDO orderDO = new OrderDO(); orderDO.setOrderId(rs.getString("order_id")); orderDO.setOrderNo(rs.getString("order_no")); orderDO.setStatus(rs.getString("status")); orderDO.setCreateDate(rs.getTimestamp("create_date")); result.add(orderDO); } return result; } catch (SQLException e) { throw new DAOException("查询订单失败", e); } finally { closeQuietly(rs, ps, conn); } } }这段代码里有个细节值得说:动态SQL的拼接。用StringBuilder拼接SQL,用List收集参数,这是最朴素但也最可控的方式。注意SQL里用了?占位符,参数通过PreparedStatement设置,这样可以防止SQL注入。千万不要用字符串拼接的方式把参数直接拼到SQL里,那是个严重的安全隐患。
3.4 参数传递与类型转换的注意事项
EOS7.5平台在调用手写接口时,参数是通过上下文对象传递的。上下文里的参数类型可能是String、Map、List等,需要开发者自己转换成业务需要的类型。这个转换过程有几个坑。
第一个坑是日期类型。平台传过来的日期通常是字符串,格式可能是"yyyy-MM-dd"也可能是"yyyy-MM-dd HH:mm:ss",需要做兼容处理。我的做法是写一个工具方法,尝试多种格式解析,解析失败再抛异常。
第二个坑是数字类型。平台传过来的数字可能是String,也可能是BigDecimal,直接强转可能报ClassCastException。稳妥的做法是用String.valueOf()转成字符串,再用Integer.parseInt()或new BigDecimal()转换。
第三个坑是集合类型。平台传过来的List,里面的元素可能是Map,也可能是String数组。需要根据实际情况做判断和转换。我通常会写一个通用的转换方法,处理常见的几种情况。
public class ParamUtil { public static Date parseDate(Object obj) { if (obj == null) return null; if (obj instanceof Date) return (Date) obj; String str = String.valueOf(obj).trim(); if (str.isEmpty()) return null; String[] patterns = {"yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd", "yyyy/MM/dd"}; for (String pattern : patterns) { try { return new SimpleDateFormat(pattern).parse(str); } catch (ParseException e) { // 继续尝试下一个格式 } } throw new IllegalArgumentException("无法解析日期: " + str); } public static Integer parseInt(Object obj) { if (obj == null) return null; if (obj instanceof Number) return ((Number) obj).intValue(); String str = String.valueOf(obj).trim(); if (str.isEmpty()) return null; return Integer.parseInt(str); } }提示:参数转换的代码虽然简单,但一定要写单元测试。我见过太多因为参数类型问题导致的线上故障,都是因为开发时没测边界情况。
4. 完整实操流程与关键环节实现
4.1 项目结构与依赖配置
在EOS7.5里做手写接口开发,项目结构建议这样组织:
src/ main/ java/ com/example/eos/ flow/ # Controller层,平台接口实现 QueryOrderListFlow.java BatchUpdateFlow.java service/ # Service层 OrderService.java impl/ OrderServiceImpl.java dao/ # DAO层 OrderDAO.java impl/ OrderDAOImpl.java model/ # 数据模型 OrderDO.java OrderDTO.java util/ # 工具类 DataSourceUtil.java ParamUtil.java exception/ # 自定义异常 ServiceException.java DAOException.java resources/ logicflow-config.xml # 接口注册配置依赖配置方面,需要引入平台的SDK包,通常是eos-system-sdk.jar或类似名称。这个包提供了ILogicFlow接口、LogicFlowContext类等平台API。另外需要引入数据库驱动包,比如MySQL的mysql-connector-java。如果用了连接池,还需要引入连接池的依赖。
<dependencies> <dependency> <groupId>com.eos</groupId> <artifactId>eos-system-sdk</artifactId> <version>7.5.0</version> <scope>provided</scope> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.28</version> </dependency> </dependencies>注意平台SDK的scope要设为provided,因为运行时平台会提供这些类,打包时不需要打进去,否则可能和平台的类冲突。
4.2 接口注册配置的写法
接口写完之后,需要在logicflow-config.xml里注册。配置的格式大致如下:
<logicflows> <logicflow id="queryOrderList" name="查询订单列表" class="com.example.eos.flow.QueryOrderListFlow"> <inputs> <input name="orderNo" type="string" label="订单编号"/> <input name="startDate" type="string" label="开始日期"/> </inputs> <outputs> <output name="result" type="list" label="订单列表"/> </outputs> </logicflow> </logicflows>配置里的id是逻辑流设计器里显示的唯一标识,class是实现类的全限定名。inputs和outputs定义了接口的输入输出参数,这些定义会显示在逻辑流设计器里,方便编排时使用。
有个细节要注意:配置文件的路径和名称可能因平台版本不同而有差异。有的版本是放在WEB-INF/classes下,有的版本是通过平台的管理界面导入。建议先查一下项目里已有的配置文件,照着已有的格式写。
4.3 一个完整的查询接口实现
下面用一个完整的例子串起来。需求是:根据订单编号和日期范围查询订单列表,返回订单的基本信息。
首先是Controller层:
public class QueryOrderListFlow implements ILogicFlow { private OrderService orderService = new OrderServiceImpl(); @Override public Object execute(LogicFlowContext context) throws Exception { // 1. 获取参数 String orderNo = (String) context.getInput("orderNo"); String startDateStr = (String) context.getInput("startDate"); String endDateStr = (String) context.getInput("endDate"); // 2. 参数转换 Date startDate = ParamUtil.parseDate(startDateStr); Date endDate = ParamUtil.parseDate(endDateStr); // 3. 调用Service List<OrderDTO> orders = orderService.queryOrders(orderNo, startDate, endDate); // 4. 组装返回结果 Map<String, Object> result = new HashMap<>(); result.put("total", orders.size()); result.put("list", orders); return result; } }然后是Service层:
public class OrderServiceImpl implements OrderService { private OrderDAO orderDAO = new OrderDAOImpl(); @Override public List<OrderDTO> queryOrders(String orderNo, Date startDate, Date endDate) throws ServiceException { try { // 参数校验 if (startDate != null && endDate != null && startDate.after(endDate)) { throw new ServiceException("开始日期不能晚于结束日期"); } // 调用DAO List<OrderDO> doList = orderDAO.selectByCondition(orderNo, startDate, endDate); // DO转DTO List<OrderDTO> dtoList = new ArrayList<>(); for (OrderDO orderDO : doList) { OrderDTO dto = new OrderDTO(); dto.setOrderId(orderDO.getOrderId()); dto.setOrderNo(orderDO.getOrderNo()); dto.setStatus(orderDO.getStatus()); dto.setCreateDate(orderDO.getCreateDate()); dtoList.add(dto); } return dtoList; } catch (DAOException e) { throw new ServiceException("查询订单失败", e); } } }最后是DAO层:
public class OrderDAOImpl implements OrderDAO { @Override public List<OrderDO> selectByCondition(String orderNo, Date startDate, Date endDate) throws DAOException { StringBuilder sql = new StringBuilder( "SELECT order_id, order_no, status, create_date FROM t_order WHERE 1=1 "); List<Object> params = new ArrayList<>(); if (orderNo != null && !orderNo.isEmpty()) { sql.append("AND order_no LIKE ? "); params.add("%" + orderNo + "%"); } if (startDate != null) { sql.append("AND create_date >= ? "); params.add(startDate); } if (endDate != null) { sql.append("AND create_date <= ? "); params.add(endDate); } sql.append("ORDER BY create_date DESC LIMIT 1000"); Connection conn = null; PreparedStatement ps = null; ResultSet rs = null; try { conn = DataSourceUtil.getConnection(); ps = conn.prepareStatement(sql.toString()); for (int i = 0; i < params.size(); i++) { ps.setObject(i + 1, params.get(i)); } rs = ps.executeQuery(); List<OrderDO> result = new ArrayList<>(); while (rs.next()) { OrderDO orderDO = new OrderDO(); orderDO.setOrderId(rs.getString("order_id")); orderDO.setOrderNo(rs.getString("order_no")); orderDO.setStatus(rs.getString("status")); orderDO.setCreateDate(rs.getTimestamp("create_date")); result.add(orderDO); } return result; } catch (SQLException e) { throw new DAOException("查询订单失败: " + e.getMessage(), e); } finally { closeQuietly(rs, ps, conn); } } }这个例子虽然简单,但完整展示了三层架构的协作方式。Controller层负责适配平台,Service层负责业务逻辑,DAO层负责数据访问。每一层的职责清晰,代码可读性和可维护性都不错。
4.4 批量操作的实现与优化
批量操作是手写接口的常见场景。比如批量更新订单状态、批量导入数据、批量发送通知等。批量操作的实现要点是:控制批次大小、处理部分失败、优化数据库交互。
public class BatchUpdateStatusFlow implements ILogicFlow { private OrderService orderService = new OrderServiceImpl(); @Override public Object execute(LogicFlowContext context) throws Exception { List<String> orderIds = (List<String>) context.getInput("orderIds"); String status = (String) context.getInput("status"); if (orderIds == null || orderIds.isEmpty()) { throw new IllegalArgumentException("订单ID列表不能为空"); } // 分批处理,每批500条 int batchSize = 500; int successCount = 0; List<String> failedIds = new ArrayList<>(); for (int i = 0; i < orderIds.size(); i += batchSize) { int end = Math.min(i + batchSize, orderIds.size()); List<String> batch = orderIds.subList(i, end); try { orderService.batchUpdateStatus(batch, status); successCount += batch.size(); } catch (ServiceException e) { failedIds.addAll(batch); } } Map<String, Object> result = new HashMap<>(); result.put("successCount", successCount); result.put("failedIds", failedIds); return result; } }批量操作的优化点有几个:第一,用PreparedStatement的addBatch()和executeBatch(),比循环单条执行快很多;第二,批次大小要合理,太小了网络往返次数多,太大了内存占用高,一般500到1000比较合适;第三,部分失败的处理策略要明确,是全部回滚还是跳过失败继续执行,根据业务需求决定。
5. 常见问题与排查技巧实录
5.1 接口注册后不生效的排查思路
这是最常见的问题之一。接口写好了,配置文件也改了,但逻辑流设计器里就是看不到,或者调用时报"接口不存在"。
排查步骤是这样的:第一步,检查配置文件的路径和名称是否正确。EOS7.5不同版本对配置文件的加载路径可能不同,有的在WEB-INF/classes下,有的在WEB-INF/config下。可以看一下平台启动日志,通常会打印加载了哪些配置文件。
第二步,检查配置文件的格式是否正确。XML的标签名、属性名、命名空间都要对。一个常见的错误是class属性写成了className,或者id重复了。可以用XML校验工具检查一下格式。
第三步,检查实现类是否正确实现了平台接口。类名、包名、方法签名都要对。如果实现类编译不通过,平台加载时会报ClassNotFoundException。
第四步,检查平台是否重启了。EOS7.5的接口注册通常需要重启平台才能生效,热部署的支持有限。改完配置后记得重启。
5.2 参数获取为null的常见原因
参数获取为null,通常有几种情况:一是逻辑流编排时没有正确传递参数,检查一下上游节点的输出是否连接到了这个接口的输入;二是参数名不匹配,配置里定义的输入名和context.getInput()里用的名字要一致;三是参数类型不匹配,平台传过来的是String,代码里按Integer取,会得到null。
我的排查习惯是,在execute方法的第一行加日志,把context里的所有参数打印出来。这样一眼就能看出平台到底传了什么过来。
@Override public Object execute(LogicFlowContext context) throws Exception { // 调试日志:打印所有输入参数 Map<String, Object> inputs = context.getInputs(); for (Map.Entry<String, Object> entry : inputs.entrySet()) { System.out.println("输入参数: " + entry.getKey() + " = " + entry.getValue() + " (类型: " + (entry.getValue() == null ? "null" : entry.getValue().getClass().getName()) + ")"); } // ... 后续逻辑 }提示:调试日志在开发阶段很有用,但上线前记得删掉或者改成debug级别,避免日志量过大。
5.3 数据库连接泄漏的预防与检测
手写DAO层最容易出的问题就是连接泄漏。Connection、PreparedStatement、ResultSet这三个资源,用完必须关闭。如果某个分支抛异常了,没有走到关闭逻辑,连接就泄漏了。连接池的连接是有限的,泄漏几个可能没事,泄漏多了系统就挂了。
预防连接泄漏的标准做法是用try-finally块,在finally里关闭资源。关闭的顺序是ResultSet、PreparedStatement、Connection,和打开的顺序相反。关闭的时候要判空,并且捕获异常,避免关闭时的异常覆盖了业务异常。
public static void closeQuietly(ResultSet rs, PreparedStatement ps, Connection conn) { if (rs != null) { try { rs.close(); } catch (SQLException e) { /* 记录日志 */ } } if (ps != null) { try { ps.close(); } catch (SQLException e) { /* 记录日志 */ } } if (conn != null) { try { conn.close(); } catch (SQLException e) { /* 记录日志 */ } } }检测连接泄漏的方法:一是看连接池的监控指标,活跃连接数持续增长不下降,大概率是泄漏了;二是用连接池的泄漏检测功能,比如Druid的removeAbandoned配置,可以自动回收长时间未关闭的连接;三是在代码里加日志,记录连接的获取和关闭,对比数量。
5.4 性能问题的定位与优化
手写接口的性能问题,通常出在数据库查询上。常见的性能瓶颈有:没有走索引、返回数据量过大、循环里查数据库(N+1问题)、事务范围过大。
定位性能问题的第一步是加耗时日志。在Service层和DAO层的关键方法入口和出口记录时间戳,算出耗时。这样能快速定位到是哪一层慢。
long start = System.currentTimeMillis(); List<OrderDO> doList = orderDAO.selectByCondition(orderNo, startDate, endDate); long cost = System.currentTimeMillis() - start; if (cost > 1000) { System.out.println("慢查询警告: selectByCondition 耗时 " + cost + "ms, 参数: " + orderNo + ", " + startDate + ", " + endDate); }优化手段方面:没有索引就加索引,用EXPLAIN分析SQL执行计划;返回数据量过大就加分页,限制单次返回条数;N+1问题就改成批量查询,一次查出来在内存里组装;事务范围过大就缩小事务粒度,只把必须在一个事务里的操作放进去。
5.5 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 接口注册后不生效 | 配置文件路径错误 | 查看平台启动日志 | 确认配置文件加载路径 |
| 接口注册后不生效 | 实现类未实现平台接口 | 检查类声明和编译结果 | 修正类声明,重新编译 |
| 参数获取为null | 参数名不匹配 | 打印context所有参数 | 统一配置和代码中的参数名 |
| 参数获取为null | 上游节点未传参 | 检查逻辑流编排 | 连接上游输出到本接口输入 |
| 数据库连接泄漏 | 异常分支未关闭连接 | 监控连接池活跃数 | 用try-finally确保关闭 |
| 查询性能差 | 未走索引 | EXPLAIN分析执行计划 | 添加合适的索引 |
| 批量操作超时 | 批次过大 | 查看单次执行耗时 | 减小批次大小,分批提交 |
| 事务回滚不生效 | 异常被吞掉 | 检查异常处理逻辑 | 确保异常向上抛出 |
6. 代码组织与维护的实战心得
6.1 包结构与命名规范
三层架构的包结构,我习惯按层分包:flow包放Controller层的类,service包放Service接口,service.impl放Service实现,dao包放DAO接口,dao.impl放DAO实现,model包放数据模型,util包放工具类,exception包放自定义异常。
命名规范方面,Controller层的类以Flow结尾,比如QueryOrderListFlow;Service接口以Service结尾,实现类加Impl后缀;DAO接口以DAO结尾,实现类加Impl后缀;数据模型,数据库映射对象以DO结尾,传输对象以DTO结尾。
这套规范不是强制的,但团队里统一了之后,看类名就知道它属于哪一层、干什么用的,维护效率会高很多。
6.2 日志记录的分层策略
日志是排查问题的关键。我的做法是:Controller层记录接口的调用和返回,Service层记录业务逻辑的关键节点,DAO层记录SQL和参数。日志级别方面,正常的调用记录用INFO,异常用ERROR,调试信息用DEBUG。
// Controller层 logger.info("查询订单列表, 参数: orderNo={}, startDate={}", orderNo, startDate); // Service层 logger.info("查询订单, 条件: orderNo={}, startDate={}, endDate={}", orderNo, startDate, endDate); // DAO层 logger.debug("执行SQL: {}, 参数: {}", sql, params);日志里不要打印敏感信息,比如密码、身份证号、银行卡号。如果确实需要记录,做脱敏处理。
6.3 单元测试的编写要点
三层架构的一个好处是方便单元测试。Service层和DAO层可以脱离平台独立测试。Service层的测试用Mock替换DAO,验证业务逻辑;DAO层的测试用内存数据库(比如H2)或者测试库,验证SQL的正确性。
public class OrderServiceTest { @Test public void testQueryOrders_DateRangeInvalid() { OrderService service = new OrderServiceImpl(); Date startDate = new Date(2024, 1, 1); Date endDate = new Date(2023, 1, 1); try { service.queryOrders(null, startDate, endDate); fail("应该抛出ServiceException"); } catch (ServiceException e) { assertEquals("开始日期不能晚于结束日期", e.getMessage()); } } }单元测试不用追求100%覆盖率,但核心的业务逻辑、边界条件、异常分支一定要覆盖。我见过太多因为没测边界条件导致的线上问题,事后补测试的成本远高于事前写测试。
6.4 版本管理与协作规范
手写接口的代码建议用Git管理,和平台的可视化配置分开仓库或者分开目录。因为可视化配置通常是XML文件,手写代码是Java文件,两者的变更频率和评审方式不同。
提交代码时,commit message要写清楚改了什么、为什么改。比如"修复查询订单接口日期参数解析异常的问题"比"修改代码"有用得多。代码评审时,重点看业务逻辑是否正确、异常处理是否完善、SQL是否有性能问题。
提示:如果团队里有多个人同时开发手写接口,建议约定好包名和类名的命名规则,避免冲突。比如按模块划分包名,订单相关的放
order包,用户相关的放user包。
7. 一些踩坑后的个人体会
三层架构在EOS7.5的手写接口开发里,不是什么高深的技术,但确实能解决实际问题。我最大的体会是:分层不是为了好看,是为了隔离变化。平台升级了,可能只需要改Controller层的适配代码;业务规则变了,只改Service层;数据库换了,只改DAO层。每一层的变更不会波及到其他层,这就是分层的价值。
另一个体会是,不要过度设计。有的项目里,一个简单的查询接口,非要搞一堆抽象类和接口,代码量翻了好几倍,维护起来反而更累。三层架构是够用的,不需要再往上加更多的层。Controller薄一点,Service厚一点,DAO简单一点,这个比例比较合适。
最后说一个实际踩过的坑:有一次在DAO层用了连接池的连接,但是在一个循环里反复获取连接,没有及时关闭,导致连接池耗尽,整个系统卡死。后来改成在循环外获取一次连接,循环内复用,问题就解决了。这个教训让我养成了一个习惯:写DAO层代码时,先想清楚连接的获取和释放时机,再动手写。
代码这东西,写出来是给人看的,顺便给机器执行。三层架构让代码的结构更清晰,别人接手的时候能快速看懂,自己过几个月回来看也能快速回忆起来。在低代码平台里手写接口,本身就是在"效率"和"灵活"之间找平衡,三层架构是那个让平衡点更稳的支点。