Java开发经验——阿里巴巴编码规范实践解析8

摘要

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

1. 【强制】在表查询中,一律不要使用 * 作为查询的字段列表,需要哪些字段必须明确写明。

说明:

  1. 增加查询分析器解析成本。
  2. 增减字段容易与 resultMap 配置不一致。
  3. 无用字段增加网络消耗,尤其是 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 驱动下映射不同,可能是 LongBigInteger,导致强转异常。 |
| 缺乏编译期检查 | 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 层:通用业务处理层,它有如下特征
    1. 对第三方平台封装的层,预处理返回结果及转化异常信息,适配上层接口。
    2. 对 Service 层通用能力的下沉,如缓存方案、中间件通用处理。
    3. 与 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规范设计》

相关推荐
小阳拱白菜16 分钟前
intell JIDEAL的快捷键
java
匆匆整棹还16 分钟前
idea配置android--以idea2023为例
android·java·intellij-idea
goldfishsky17 分钟前
elasticsearch
开发语言·数据库·python
梦想实现家_Z18 分钟前
拆解Java MCP Server SSE代码
java·spring·mcp
梦想实现家_Z26 分钟前
原生Java SDK实现MCP Server(基于WebMvc的SSE通信方式)
java·spring·mcp
梦想实现家_Z28 分钟前
原生Java SDK实现MCP Server(基于WebFlux的SSE通信方式)
java·spring·mcp
Maỿbe29 分钟前
线程池的详细知识(含有工厂模式)
java·线程·线程池·工厂模式
梦想实现家_Z30 分钟前
原生Java SDK实现MCP Server(基于Servlet的SSE通信方式)
java·spring·mcp
梦想实现家_Z37 分钟前
原生Java SDK实现MCP Server(Stdio的通信方式)
java·spring·mcp
菜一头包43 分钟前
CPP中CAS std::chrono 信号量与Any类的手动实现
开发语言·c++