摘要
本文主要介绍了阿里巴巴编码规范在Java开发中的实践解析,强调了在表查询中不使用"*"作为查询字段列表的重要性,指出其会增加查询分析成本、浪费网络传输资源、降低可维护性、不利于缓存且存在安全隐患。同时,还提出了正例推荐写法,包括明确指定查询字段、在不同框架中的体现以及最佳实践建议。此外,还涉及了POJO类布尔属性命名规范、resultMap的使用、sql.xml配置参数的规范、数据更新接口的规范、事务的合理使用、分层结构推荐、异常处理规约以及分层领域模型规约等内容,旨在帮助Java开发者更好地遵循编码规范,提高代码质量和可维护性。

1. 【强制】在表查询中,一律不要使用 * 作为查询的字段列表,需要哪些字段必须明确写明。
说明:
- 增加查询分析器解析成本。
- 增减字段容易与 resultMap 配置不一致。
- 无用字段增加网络消耗,尤其是 text 类型的字段。
1.1. 为什么不能使用 SELECT *
?
|----------|----------------------------------------------------|
| 问题类别 | 详细说明 |
| ❌ 性能问题 | 解析 SQL 时会去查字段元数据,字段越多解析成本越大,尤其在复杂表连接中更明显。 |
| ❌ 网络传输浪费 | 加载了不需要的字段(如 TEXT、BLOB 等大字段),增加了数据库到应用间的网络 IO。 |
| ❌ 可维护性差 | 表结构变更(加字段、删字段)时容易造成 resultMap
、DTO 等解析失败或字段映射错乱。 |
| ❌ 难以做缓存 | 无法根据字段内容判断缓存命中,增加缓存粒度模糊性。 |
| ❌ 安全隐患 | 返回了原本不该暴露给前端/日志的敏感字段,例如密码、身份证、手机号等。 |
1.2. ✅ 正例推荐写法
1.2.1. ✅ 推荐 SQL 写法(明确字段)
SELECT id, name, email FROM user WHERE status = 1;
1.2.2. ❌ 错误写法(不可控)
SELECT * FROM user WHERE status = 1;
1.3. ✅ 在不同框架中的体现
1.3.1. MyBatis
<!-- resultMap 需字段一一对应 -->
<select id="getUser" resultMap="BaseResultMap">
SELECT id, name, email FROM user WHERE id = #{id}
</select>
1.3.2. JPA / ORM
-
虽然有默认实体映射,但最好使用 JPQL 或 Projection 显式列出字段:
@Query("SELECT u.id, u.name FROM User u WHERE u.status = 1")
List<UserDTO> findActiveUsers();
1.4. ✅ 最佳实践建议
|-----------|--------------------------|
| 场景 | 建议 |
| DTO/VO 场景 | 只查询所需字段,避免加载多余字段,提升效率 |
| 高并发接口 | 尤其注意排除 TEXT/BLOB 等大字段 |
| 日志与敏感数据 | 严格控制查询字段,避免敏感信息误打印到日志或返回 |
2. 【强制】POJO 类的布尔属性不能加 is,而数据库字段必须加 is_,要求在 resultMap 中进行字段与属性之间的映射。
说明:参见定义 POJO 类以及数据库字段定义规定,在 sql.xml 增加映射,是必须的。
3. 【强制】不要用 resultClass 当返回参数,即使所有类属性名与数据库字段一一对应,也需要定义<resultMap>;反过来,每一个表也必然有一个<resultMap>与之对应。
说明:配置映射关系,使字段与 DO 类解耦,方便维护。
3.1. ✅ 规范要点总结
- 不要用 resultType****或 resultClass****直接映射返回结果类。
- 即使 POJO 属性名和数据库字段名完全一致,也要定义
<resultMap>
。 - 每张表对应至少一个
<resultMap>
映射,保证灵活维护。 <resultMap>
使数据库字段与对象字段的映射关系显式配置,实现解耦。
3.2. ❌ 为什么不推荐直接使用 resultType
/ resultClass
|----------|--------------------------------------|
| 问题点 | 说明 |
| ❌ 隐藏映射规则 | 直接用类名,MyBatis 默认使用驼峰映射,字段名改变难定位映射问题。 |
| ❌ 难以维护 | 字段变动时,代码容易发生隐性错误,且调试难,错误不易发现。 |
| ❌ 无法灵活定制 | <resultMap>
可以手动调整字段名映射、类型转换、嵌套映射等。 |
| ❌ 代码耦合度高 | 类直接依赖数据库字段结构,耦合紧密,难以改动数据库或类结构。 |
3.3. ✅ 正确使用 <resultMap>
示例
<resultMap id="UserResultMap" type="com.example.domain.UserDO">
<id property="id" column="user_id" />
<result property="username" column="user_name" />
<result property="email" column="email_address" />
<result property="createdAt" column="created_at" />
</resultMap>
<select id="selectUser" resultMap="UserResultMap">
SELECT user_id, user_name, email_address, created_at FROM user WHERE user_id = #{id}
</select>
3.4. ✅ 四、优点
- 解耦 :数据库字段名改变,不影响 Java 类,只改
<resultMap>
映射即可。 - 灵活:支持字段重命名、复杂类型转换、嵌套关联映射。
- 可维护:映射关系集中管理,方便排查和修改。
- 规范化:符合大型项目的代码标准和规范,便于多人协作。
总结:无论字段名是否一一对应,都要定义 <resultMap>****,杜绝直接使用 resultType**/** resultClass**,保证映射的灵活性与系统的可维护性。**
4. 【强制】sql.xml 配置参数使用:#{},#param# 不要使用 ${} 此种方式容易出现 SQL 注入。
5. 【强制】iBATIS 自带的 queryForList(String statementName,int start,int size) 不推荐使用。
说明:其实现方式是在数据库取到 statementName 对应的 SQL 语句的所有记录,再通过 subList 取 start,size
的子集合,线上因为这个原因曾经出现过 OOM。
正例:
Map<String,Object> map = new HashMap<>(16);
map.put("start", start);
map.put("size", size);
6. 【强制】不允许直接拿 HashMap 与 Hashtable 作为查询结果集的输出。
反例:某同学为避免写一个<resultMap>xxx</resultMap>,直接使用 Hashtable 来接收数据库返回结果,结果出现日常是把 bigint 转成 Long 值,而线上由于数据库版本不一样,解析成 BigInteger,导致线上问题。
6.1. ✅ 规范核心说明
- 禁止用 HashMap**、** Hashtable****直接作为查询结果类型。
- 即使临时方便,也会导致类型转换不确定,增加线上问题风险。
- 需通过明确的实体类(DO/DTO)+
<resultMap>
来规范映射。
6.2. 为什么禁止用 HashMap
/Hashtable
?
|---------|--------------------------------------------------------------------------|
| 风险点 | 说明 |
| 类型不确定 | 数据库数值类型(如 bigint)在不同数据库版本或 JDBC 驱动下映射不同,可能是 Long
或 BigInteger
,导致强转异常。 |
| 缺乏编译期检查 | HashMap<String, Object>
无法在编译阶段发现字段名拼写错误或类型错误。 |
| 可维护性差 | 难以追踪和管理字段,后期变更数据库结构或字段时异常隐蔽。 |
| 严重线上隐患 | 不同环境中类型映射不一致,可能导致应用崩溃或数据处理错误。 |
6.3. ✅ 推荐做法
使用实体类 + <resultMap>
<resultMap id="UserResultMap" type="com.example.domain.UserDO">
<id property="id" column="user_id"/>
<result property="name" column="user_name"/>
<result property="age" column="user_age"/>
</resultMap>
对应 Java 实体类:
public class UserDO {
private Long id;
private String name;
private Integer age;
// getter/setter
}
如果必须使用动态字段,可考虑使用专门的 VO 或 DTO 类,而非原生 Map。
总结:禁止使用 HashMap****和 Hashtable****作为查询结果,统一使用实体类映射,避免数据类型不一致和难以维护的问题。
7. 【强制】更新数据表记录时,必须同时更新记录对应的 update_time 字段值为当前时间。
8. 【推荐】不要写一个大而全的数据更新接口。 传入为 POJO 类,不管是不是自己的目标更新字段,都进行update table set c1 = value1 , c2 = value2 , c3 = value3;这是不对的。执行 SQL 时,不要更新无改动的字段,一是易出错;二是效率低;三是增加 binlog 存储。
"更新操作只更新实际变动的字段,避免大而全、无差别地全字段更新。"
8.1. ✅规范核心含义
不推荐:
update user set name = #{name}, email = #{email}, age = #{age} where id = #{id};
即使这些字段值根本没有变化,也全部更新。
推荐:
<set>
<if test="name != null"> name = #{name}, </if>
<if test="email != null"> email = #{email}, </if>
<if test="age != null"> age = #{age} </if>
</set>
仅更新真正需要改动的字段。
8.2. 为什么不能"无差别更新"?
|----------------|-----------------------------------------------|
| 问题点 | 说明 |
| ❌易误更新 | POJO 被错误填充默认值(如 0/null/""),可能导致非目标字段被误更新 |
| ❌性能浪费 | 无差别 UPDATE 会修改整行数据,增加锁竞争,降低写入效率 |
| ❌binlog 膨胀 | MySQL 会记录每次更新的全部字段,即使值没变,也会产生完整 binlog |
| ❌主从同步压力 | Binlog 体积大,导致主从同步延迟增加 |
| ❌容易丢数据 | 特别是在更新对象未做字段控制时,容易清空其他字段原始值(比如某些字段传 null) |
8.3. ✅ 正确写法(以 MyBatis 为例)
<update id="updateUser" parameterType="com.example.User">
update user
<set>
<if test="name != null"> name = #{name}, </if>
<if test="email != null"> email = #{email}, </if>
<if test="age != null"> age = #{age} </if>
</set>
where id = #{id}
</update>
8.4. ✅ 最佳实践建议
|-----------|-----------------------------------------------|
| 场景 | 建议 |
| DTO 层 | 拆分多个"更新专用 DTO",不要直接用全量 POJO |
| 接口层 | 拆分为:修改基本信息、修改密码、修改头像等小接口 |
| MyBatis 层 | 用 <if>
标签拼接动态 SQL,仅更新需要的字段 |
| JPA 层 | 尽量用 @Modifying @Query
指定更新字段而不是 save(entity) |
9. 【参考】@Transactional 事务不要滥用。 事务会影响数据库的 QPS,另外使用事务的地方需要考虑各方面的回滚方案,包括缓存回滚、搜索引擎回滚、消息补偿、统计修正等。
不要滥用 @Transactional**,事务不是免费使用的,代价高、责任重。**
9.1. ✅为什么要谨慎使用事务?
|---------------|-----------------------------------------------------------------------------|
| 问题 | 说明 |
| 📉性能损耗 | 事务开启后,MySQL/InnoDB 要维护 undo/redo log、加锁、限制并发,导致 QPS 降低。 |
| 🔒资源占用 | 长事务会持有数据库连接、锁资源,造成阻塞甚至死锁。 |
| ❗不完整的回滚 | 大多数情况下, @Transactional 只保证数据库回滚, 缓存、消息队列、搜索引擎等都不会自动回滚 。 |
| 💥全局异常吞噬 | 某些异常(如 catch 住、或非 RuntimeException)不会触发事务回滚,易留坑。 |
| 🧩分布式事务困难 | 一旦涉及多个系统或数据库表,事务控制难度和成本大增,可能需要引入消息补偿或 TCC 模式。 |
9.2. ✅常见滥用场景举例
|---------------------------------------|------------------------------|
| 场景 | 说明 |
| ❌在读操作上加事务 | 没必要浪费事务开销 |
| ❌Controller 层加 @Transactional | 会造成事务粒度过大,不清晰 |
| ❌包含远程调用的业务中加事务 | 事务回滚了但远程服务没法一起回滚,造成数据不一致 |
| ❌操作缓存、ES、MQ 等非数据库资源但未配套补偿机制 | 回滚不完整,业务数据不一致 |
9.3. ✅最佳实践建议
9.3.1. 控制事务范围最小化(即最小事务单元)
@Transactional
public void updateUserInfo(UserDTO userDTO) {
// 只更新 DB,缓存单独处理
userMapper.update(...);
}
9.3.2. 不跨 RPC、MQ、Redis 等资源使用事务,使用补偿机制替代
- ✅分布式事务 → 使用本地消息表 + 消息确认机制
- ✅Redis 缓存 → 明确使用 try-catch 后补偿、双写延迟队列
- ✅MQ 消息 → 使用幂等机制 + 状态标记
9.3.3. @Transactional****应只出现在Service 层,不出现在Controller和DAO层。
9.3.4. 明确哪些操作需要事务,哪些不需要
- ✅批量 insert/update/delete → 可考虑事务。
- ✅单条读操作、缓存更新、状态轮询等 → 不需要事务。
9.4. ✅额外提醒:Spring 的事务默认规则坑点
|----------------------------|----------------------------------------------------|
| 事务规则 | 说明 |
| 默认只回滚 RuntimeException | checked exception****不会回滚,除非配置 rollbackFor |
| 事务方法调用自身内部方法无效 | 事务必须通过 Spring AOP 调用才能生效 |
| 多线程场景下事务无效 | 子线程不会共享事务上下文 |
| 超时/嵌套事务不当容易造成锁未释放 | 配置不当会引发隐藏问题 |
总结:事务是业务保障的手段,不是默认配置。用事务前,请问自己:是否真的需要?事务能保护的全部资源是否都可回滚?
10. 【参考】<isEqual>中的 compareValue 是与属性值对比的常量,一般是数字,表示相等时带上此条件;<isNotEmpty>表示不为空且不为 null 时执行;<isNotNull>表示不为 null 值时执行。
11. 【推荐】根据业务架构实践,结合业界分层规范与流行技术框架分析,推荐分层结构如图所示,默认上层依赖于下层,箭头关系表示可直接依赖,如:开放 API 层可以依赖于 Web 层(Controller 层),也可以直接依赖于 Service 层,依此类推:

- 开放 API 层:可直接封装 Service 接口暴露成 RPC 接口;通过 Web 封装成 http 接口;网关控制层等。
- 终端显示层:各个端的模板渲染并执行显示的层。当前主要是 velocity 渲染,JS 渲染,JSP 渲染,移动端展示等。
- Web 层:主要是对访问控制进行转发,各类基本参数校验,或者不复用的业务简单处理等。
- Service 层:相对具体的业务逻辑服务层。
- Manager 层:通用业务处理层,它有如下特征
-
- 对第三方平台封装的层,预处理返回结果及转化异常信息,适配上层接口。
- 对 Service 层通用能力的下沉,如缓存方案、中间件通用处理。
- 与 DAO 层交互,对多个 DAO 的组合复用。
- DAO 层:数据访问层,与底层 MySQL、Oracle、Hbase、OceanBase 等进行数据交互。
- 第三方服务:包括其它部门 RPC 服务接口,基础平台,其它公司的 HTTP 接口,如淘宝开放平台、支付宝付款服务、高德地图服务等。
- 外部数据接口:外部(应用)数据存储服务提供的接口,多见于数据迁移场景中。
12. 【参考】(分层异常处理规约)在 DAO 层,产生的异常类型有很多,无法用细粒度的异常进行 catch,使用 catch(Exception e) 方式,并 throw new DAOException(e),不需要打印日志,
因为日志在Manager 或 Service 层一定需要捕获并打印到日志文件中去,如果同台服务器再打日志,浪费性能和存储。在 Service 层出现异常时,必须记录出错日志到磁盘,尽可能带上参数和上下文信息,相当于保护案发现场。Manager 层与 Service 同机部署,日志方式与 DAO 层处理一致,如果是单独部署,则采用与Service 一致的处理方式。Web 层绝不应该继续往上抛异常,因为已经处于顶层,如果意识到这个异常将导致页面无法正常渲染,那么就应该直接跳转到友好错误页面,尽量加上友好的错误提示信息。开放接口层要将异常处理成错误码和错误信息方式返回。
这是一个非常重要且成熟的分层异常处理规约 ,目标是在系统中做到:"异常分层抛出、按职责处理、按场景记录、避免重复打日志"。
12.1. ✅各层的职责与处理方式概览
|---------------|---------------------------------------------------------|---------------|----------------------------|
| 层级 | 异常处理策略 | 是否打日志 | 抛出异常类型 |
| DAO 层 | catch(Exception e) 后包装为 DAOException 抛出 | ❌不打日志 | throw new DAOException |
| Manager 层 | 可选择记录日志(若和 Service 层不同机),否则继续抛出 | ✅或❌ | 抛出封装后的业务异常 |
| Service 层 | 捕获并记录完整日志(包含参数、上下文),保证案发现场完整 | ✅必须打日志 | 抛出业务异常或处理异常 |
| Web 层 | 绝不继续向上抛出 ,处理为友好页面/响应码/提示消息 | ✅打用户友好日志 | 页面跳转或 JSON 响应 |
| 开放接口层 | 捕获所有异常,转为标准错误码和提示信息响应 | ✅日志 + 错误码 | 统一响应格式 |
12.2. ✅DAO 层示例(不打日志,只包装异常)
不使用 try-catch,而是让异常自然抛出(即直接向上抛出) ,由 Service 层统一 catch 包装为 BizException 并打印日志。为什么 DAO 层通常不写 try-catch?
|----------------|--------------------------------------------------------------------------------------|
| 原因 | 说明 |
| ✅ 避免重复封装异常 | MyBatis、JPA 已经会抛出具体的数据库异常(如 PersistenceException
, SQLException
),不需要手动再 try-catch |
| ✅ 统一异常处理职责 | DAO 只做数据访问,不处理业务,异常的封装和日志由上层(Service)负责 |
| ✅ 便于排查问题 | Service 打日志时可以带上业务上下文(userId, 操作参数),DAO 很难拿到这些信息 |
| ✅ 保持代码简洁 | 大量 try-catch 会让 DAO 代码变得冗余 |
12.3. ✅ 推荐写法(更常见的做法)
// ✅ 不做 try-catch,异常自然抛出
public UserDO getUserById(Long id) {
return sqlSession.selectOne("UserMapper.selectById", id);
}
然后在 Service 层封装日志与业务异常:
public UserDTO queryUser(Long userId) {
try {
UserDO user = userDAO.getUserById(userId);
return convert(user);
} catch (Exception e) {
log.error("查询用户失败 userId={}", userId, e);
throw new BizException("用户查询失败");
}
}
阿里规约 vs 实战最佳实践
|-----------------------------|------------------------------------|----------------------|
| 规范来源 | 是否建议 DAO try-catch | 是否建议封装为 DAOException |
| 阿里 Java 开发手册(规范型项目) | ✅ 建议 catch 后包装 DAOException(但不打日志) | 是 |
| 实战项目(Spring Boot + MyBatis) | ❌ 通常不 catch,让异常抛给 Service | 否(让 Service 层统一包装) |
12.4. 什么时候写 try,什么时候不写?
|----------------------------------------|-----------------------------------------|
| 场景 | 建议 |
| ✅ DAO 层复杂通用封装(如 BaseDAO) | 可使用 try-catch,统一转为 DAOException |
| ✅ DAO 层用 JDBC 原生写法(抛 SQLException) | 应封装为 DAOException |
| ✅ 规范驱动团队(如银行/金融) | 遵循阿里手册,catch Exception + 包装DAOException |
| ✅ 敏捷开发团队或项目较轻量 | 直接抛出异常,在 Service 层统一处理更简洁 |
12.5. ✅Service 层示例(打日志 + 上抛)
public UserDTO getUserById(Long id) {
try {
return userManager.getUserById(id);
} catch (DAOException e) {
log.error("查询用户失败,userId={},异常:{}", id, e.getMessage(), e);
throw new BizException("用户查询异常", e);
}
}
12.6. ✅Controller全局异常处理
Controller 层将异常转为响应码或错误页面 的最佳实践方式就是使用 全局异常处理(Global Exception Handler) ,通过 Spring 提供的 @ControllerAdvice
和 @ExceptionHandler
实现。
12.6.1. ✅ 为什么使用全局异常处理?
|----------------|-------------------------------------|
| 目的 | 说明 |
| 📦 统一异常格式 | 避免每个 Controller 手动 try-catch,减少重复代码 |
| 🎯 集中管理错误码 | 可以统一输出错误码、错误信息、traceId 等 |
| 😇 提升用户体验 | 前端展示统一风格的错误提示页或 JSON 格式 |
| 📄 方便监控审计 | 日志记录位置统一,便于日志采集、异常告警 |
12.6.2. ✅ 示例:全局异常处理类(针对 Web 和 API)
@RestControllerAdvice // 如果是页面应用,可用 @ControllerAdvice
public class GlobalExceptionHandler {
// 处理自定义业务异常
@ExceptionHandler(BizException.class)
public ApiResponse<?> handleBizException(BizException ex) {
log.warn("业务异常:{}", ex.getMessage(), ex);
return ApiResponse.failure("BIZ_ERROR", ex.getMessage());
}
// 处理数据库异常
@ExceptionHandler(DAOException.class)
public ApiResponse<?> handleDaoException(DAOException ex) {
log.error("DAO异常:{}", ex.getMessage(), ex);
return ApiResponse.failure("DB_ERROR", "数据库操作失败");
}
// 处理参数校验失败
@ExceptionHandler(MethodArgumentNotValidException.class)
public ApiResponse<?> handleValidation(MethodArgumentNotValidException ex) {
String msg = ex.getBindingResult().getAllErrors().get(0).getDefaultMessage();
return ApiResponse.failure("VALIDATION_ERROR", msg);
}
// 兜底处理未知异常
@ExceptionHandler(Exception.class)
public ApiResponse<?> handleException(Exception ex) {
log.error("系统异常", ex);
return ApiResponse.failure("SYSTEM_ERROR", "系统繁忙,请稍后再试");
}
}
12.6.3. ✅ 页面异常处理(跳转错误页)
@ControllerAdvice
public class PageExceptionHandler {
@ExceptionHandler(Exception.class)
public String handlePageError(Exception ex, Model model) {
model.addAttribute("errorMsg", "页面加载失败:" + ex.getMessage());
return "errorPage";
}
}
12.6.4. ✅ 示例响应体类:ApiResponse
public class ApiResponse<T> {
private String code;
private String message;
private T data;
public static <T> ApiResponse<T> success(T data) {
ApiResponse<T> res = new ApiResponse<>();
res.code = "SUCCESS";
res.message = "操作成功";
res.data = data;
return res;
}
public static <T> ApiResponse<T> failure(String code, String message) {
ApiResponse<T> res = new ApiResponse<>();
res.code = code;
res.message = message;
return res;
}
// getter/setter 略
}
12.6.5. ✅ 总结
- ✅ Controller 层异常不应使用 try-catch,而应通过 全局异常处理集中解决。
- ✅
@RestControllerAdvice
适用于前后端分离的接口(JSON 返回)。 - ✅
@ControllerAdvice
适用于传统页面应用(返回ModelAndView
或跳转页)。 - ✅ 每种异常可细分处理,记录日志 + 统一响应结构 是最佳实践。
12.7. ✅重点总结
|--------------|------------------------------------------------------------------------------|
| | 内容 |
| ❌不重复打日志 | 异常只在 Service/Web 层 打日志,避免 DAO 打日志浪费资源 |
| ✅上层处理上下文 | Service 层必须记录:入参、上下文、堆栈,便于排查 |
| ✅分层包装异常 | DAO → DAOException**,Manager/Service →** BizException**,统一异常结构** |
| ✅接口返回友好 | 页面 → 跳转友好错误页,接口 → 错误码 + 提示 |
13. DAO层是否需要使用try 捕获异常? 什么需要使用,什么时候不需要?
13.1. DAO是否捕获异常结论总结
|-----------------|--------------------------------------------|---------------------------------------------------------------------|
| 是否使用 try-catch | 使用场景 | 原因说明 |
| ❌ 不使用(推荐方式) | 80% 场景都不需要,如使用 MyBatis、JPA、Spring Data | 异常由框架抛出(如 PersistenceException
, SQLException
),由 Service 层统一处理即可 |
| ✅ 使用 | 需要封装为自定义异常(如 DAOException
),或使用 JDBC 原生操作 | 原生 JDBC 抛 SQLException,需要封装;某些规范要求分层异常 |
| ✅ 使用 | 编写框架/中间件/公共组件中的 BaseDAO | 可捕获异常统一转换(如抛 DAOException),提高通用性和一致性 |
| ✅ 使用 | 捕获数据库某些特定错误(如主键冲突)并做业务分支处理 | 需要判断 SQLState 或 errorCode 执行特定逻辑 |
13.2. ❌ 不使用 try-catch(常见写法)
public UserDO getUserById(Long id) {
return sqlSession.selectOne("UserMapper.selectById", id);
}
- 简洁清晰,异常直接抛出
- 由 Service 层统一记录日志、抛业务异常
- 实际项目中更常见 ✅
13.3. ✅ 使用 try-catch 的场景一:需要包装为自定义异常
如果你遵循的是分层异常体系(DAO 层抛 DAOException,Service 层抛 BizException),可以这样写:
public UserDO getUserById(Long id) {
try {
return sqlSession.selectOne("UserMapper.selectById", id);
} catch (Exception e) {
// 不打日志,向上抛 DAO 异常
throw new DAOException("查询用户失败,id=" + id, e);
}
}
- 使用自定义异常
DAOException
,方便统一识别来源 - 常见于 大型公司、强规范金融类项目
- 缺点是代码冗余,Service 层还需再包装一次
13.4. ✅ 使用 try-catch 的场景二:原生 JDBC 时必须使用
public UserDO getUserById(Long id) {
Connection conn = null;
PreparedStatement stmt = null;
ResultSet rs = null;
try {
conn = dataSource.getConnection();
stmt = conn.prepareStatement("SELECT * FROM user WHERE id = ?");
stmt.setLong(1, id);
rs = stmt.executeQuery();
if (rs.next()) {
// 封装成 UserDO
return mapToUserDO(rs);
}
return null;
} catch (SQLException e) {
throw new DAOException("JDBC 查询失败", e);
} finally {
closeQuietly(rs, stmt, conn);
}
}
- 原生 JDBC 一定需要 try-catch
- 注意释放连接资源
- 推荐封装为工具类
13.5. ✅ 使用 try-catch 的场景三:针对 SQL 错误码做逻辑判断
try {
sqlSession.insert("UserMapper.insertUser", user);
} catch (DuplicateKeyException e) {
log.warn("插入用户失败,主键冲突 userId={}", user.getId());
throw new BizException("用户已存在");
}
- 某些业务逻辑上需要识别特定异常类型做降级或提示
- 通常在 Service 层做即可
13.6. 📌 总结建议:你的项目应该怎么做?
|---------------------------|-----------------------|------------------------------|
| 项目类型 | DAO 是否 try-catch | 异常处理建议 |
| 中小型项目、Spring Boot+MyBatis | ❌ 不用,异常直接抛 | Service 统一日志和封装 BizException |
| 传统企业规范项目(如金融、电信) | ✅ 使用,包装为 DAOException | 不打日志,由 Service 记录 |
| 原生 JDBC 使用较多的场景 | ✅ 必须使用 | 封装成工具类或 BaseDAO |
14. 【参考】分层领域模型规约:
- DO(Data Object):此对象与数据库表结构一一对应,通过 DAO 层向上传输数据源对象。
- DTO(Data Transfer Object):数据传输对象,Service 或 Manager 向外传输的对象。
- BO(Business Object):业务对象,可以由 Service 层输出的封装业务逻辑的对象。
- Query:数据查询对象,各层接收上层的查询请求。注意超过 2 个参数的查询封装,禁止使用 Map 类来传输。
- VO(View Object):显示层对象,通常是 Web 向模板渲染引擎层传输的对象。
博文参考
《阿里java规范设计》