编码规范是软件工程中最基础、最直接影响代码质量的工程实践。它不仅仅是"代码长什么样"的表面问题,更是关乎团队协作效率、系统可维护性、缺陷率控制的核心基础设施。本文档基于 Hugo 在支付系统、风控平台等多个金融级项目中的实践经验,结合业界成熟规范,系统性地阐述编码规范的设计原则、具体规则和实践方法。
核心理念:好的编码规范不是束缚创造力的枷锁,而是降低认知负担、提升协作效率的脚手架。它让开发者把精力集中在解决业务问题上,而不是纠结于"这行代码该怎么写"。
软件系统的复杂性往往源于层次不清。一个典型的企业级应用通常包含以下层次:
| 层次 | 职责 | 抽象粒度 | 典型技术栈 |
|---|---|---|---|
| 表现层 | 用户交互、数据展示 | 页面/组件 | React, Vue, Thymeleaf |
| 应用层 | 业务流程编排、用例协调 | 服务/用例 | Spring Service, Application Service |
| 领域层 | 核心业务逻辑、业务规则 | 领域模型/聚合 | Domain Model, Entity, Value Object |
| 基础设施层 | 数据持久化、外部通信 | 仓库/适配器 | Repository, DAO, Client |
关键原则:
每一层逻辑在对下层数据或操作进行封装时,应当尽量不对使用方如何正确使用做任何假设。同时,也应当暴露应该暴露的信息。
public class OrderRepository {
public void updateById(UUID orderId, OrderState newState) {
execute("UPDATE orders SET state = ? WHERE id = ?", newState, orderId);
// ❌ 问题:SQL更新返回的被影响行数被丢弃了
}
}
问题分析:
public class OrderRepository {
/**
* 更新订单状态
* @return 实际被更新的行数(可用于判断订单是否存在)
*/
public int updateById(UUID orderId, OrderState newState) {
return execute("UPDATE orders SET state = ? WHERE id = ?", newState, orderId);
}
}
// 上层调用方按需使用
public class OrderService {
public void cancelOrder(UUID orderId) {
int affected = orderRepository.updateById(orderId, OrderState.CANCELLED);
if (affected == 0) {
throw new OrderNotFoundException(orderId);
}
// 记录审计日志
auditLog.record("ORDER_CANCELLED", orderId);
}
}
注意:提供全信息量需要看边界。同一个服务内的类之间、不同服务之间、内部服务与外部之间,什么应该暴露、什么不应该暴露,是由很多因素共同决定的。不可因为"全信息量"的要求就无脑地对所有信息全暴露。本原则的主要目的是避免无脑地跟着当前需方的要求走,而不去认真思考自身能力输出的完备度问题。
一个类/函数应该放在哪里? 这是日常编码中最常见的决策之一。
判断标准:
常见反模式:
| 反模式 | 表现 | 后果 |
|---|---|---|
| 上帝类 | 一个类包含所有功能 | 难以维护、测试困难 |
| 散弹式修改 | 一个需求改动需要修改多个分散的类 | 遗漏风险高、维护成本高 |
| 特性依恋 | 类A的方法总是操作类B的数据 | 违反了封装原则 |
| 循环依赖 | A依赖B,B又依赖A | 编译困难、架构腐化 |
Hugo 的实践建议:
在支付系统的重构过程中,我们发现大量业务逻辑散落在 Controller 和 Service 中。通过引入领域层,将核心支付流程、状态机、金额计算等逻辑下沉到 Domain 层,不仅提升了代码的可测试性,还使得业务规则的变化不再需要修改基础设施代码。
核心思想:将变化频率高的决策逻辑(配置、策略、规则)从代码中抽离,放到外部可配置的位置。
典型场景:
| 决策类型 | 外置方式 | 示例 |
|---|---|---|
| 业务规则 | 规则引擎/Drools | 风控规则、审批流程 |
| 阈值配置 | 配置中心/Nacos | 超时时间、重试次数、限流阈值 |
| 策略选择 | 策略模式 + 配置 | 支付方式选择、路由策略 |
| 开关控制 | 特性开关/Feature Toggle | 新功能灰度、降级开关 |
实现示例:
// ❌ 硬编码决策
public PaymentResult process(PaymentRequest request) {
if (request.getAmount() > 10000) {
// 需要额外风控审核
return riskService.audit(request);
}
return normalService.process(request);
}
// ✅ 决策外置
public PaymentResult process(PaymentRequest request) {
RiskStrategy strategy = riskConfig.getStrategy(request.getPaymentType());
if (strategy.shouldAudit(request)) {
return riskService.audit(request, strategy.getAuditLevel());
}
return normalService.process(request);
}
"There are only two hard things in Computer Science: cache invalidation and naming things." — Phil Karlton
命名是代码中最重要的沟通工具。好的命名应该:
// ❌ 形式化但无意义
List<String> list1 = new ArrayList<>();
Map<String, Object> map2 = new HashMap<>();
// ✅ 语义化命名
List<String> activeMerchantIds = new ArrayList<>();
Map<String, PaymentConfig> paymentConfigByChannel = new HashMap<>();
| ❌ 不推荐 | ✅ 推荐 | 原因 |
|---|---|---|
cnt |
count / total |
不清晰 |
amt |
amount |
行业缩写,但可读性差 |
no |
number / count |
易与"否"混淆 |
flg |
flag / isXXX |
无意义缩写 |
tmp |
temporary / buffer |
临时变量也需要语义 |
例外:行业通用缩写可以使用,但需要在团队内统一
id (identifier)url / uridto / vo / daosql / json / xml// 结构:域名倒序 + 项目名 + 模块名 + 层级
com.company.project.module.layer
// 示例
com.baofu.payment.risk.domain // 领域层
com.baofu.payment.risk.application // 应用层
com.baofu.payment.risk.infrastructure.persistence // 基础设施-持久化
规则:
com.example.order 而非 com.example.orders)// 名词或名词短语,首字母大写,驼峰式
public class OrderService { }
public class PaymentTransaction { }
public interface RiskEvaluator { }
// 特定后缀约定
public class OrderRepository { } // 仓库:数据访问
public class OrderDTO { } // DTO:数据传输对象
public class OrderVO { } // VO:视图对象
public class OrderFactory { } // Factory:工厂类
public class OrderBuilder { } // Builder:构建器
public class OrderException { } // Exception:异常
// 动词或动词短语,首字母小写,驼峰式
// 查询类(无副作用,返回结果)
public Order findById(Long id) { }
public boolean existsByOrderNo(String orderNo) { }
public List<Order> listByStatus(OrderStatus status) { }
// 操作类(有副作用)
public void cancelOrder(Long orderId) { }
public Order createOrder(CreateOrderRequest request) { }
public void updateStatus(Long orderId, OrderStatus newStatus) { }
// 判断类(返回布尔值)
public boolean isValid() { }
public boolean canCancel() { }
public boolean hasRefund() { }
// 转换类
public OrderDTO toDTO() { }
public static Order fromDTO(OrderDTO dto) { }
Hugo 踩坑记录:
在早期项目中,我们使用了
getXXX()方法来执行数据库查询,导致新团队成员误以为这是简单的 getter。后来统一约定:getXXX()仅用于属性访问,数据查询使用findXXX()/queryXXX()/loadXXX(),明显区分。
// 成员变量
private final OrderRepository orderRepository;
private volatile boolean isRunning;
// 局部变量
Order order = orderRepository.findById(orderId);
List<OrderItem> orderItems = itemRepository.listByOrderId(orderId);
// 常量
public static final int MAX_RETRY_COUNT = 3;
public static final String DEFAULT_CURRENCY = "CNY";
public static final BigDecimal ZERO_AMOUNT = BigDecimal.ZERO;
// ✅ 使用肯定语气,避免否定命名
private boolean isActive; // 是否激活
private boolean hasRefund; // 是否有退款
private boolean canModify; // 是否可修改
private boolean isInitialized; // 是否已初始化
// ❌ 避免否定命名(容易搞混)
private boolean isNotValid; // 双重否定:!isNotValid = valid?
private boolean noError; // confusing
团队级词汇表:
| 概念 | 统一用词 | 避免使用 |
|---|---|---|
| 获取单个对象 | findById, loadById |
get, query |
| 获取列表 | listByXXX, findAllByXXX |
getAll, queryAll |
| 创建 | create, register, submit |
add, new |
| 更新 | update, modify |
edit, change |
| 删除 | delete, remove |
del, destroy |
| 保存 | save |
store, persist |
| 校验 | validate, check |
verify (语义不同) |
// ✅ K&R 风格(推荐)
public class OrderService {
public Order findById(Long id) {
if (id == null) {
throw new IllegalArgumentException("Order ID cannot be null");
}
return orderRepository.findById(id)
.orElseThrow(() -> new OrderNotFoundException(id));
}
}
// ❌ Allman 风格(Java 社区较少使用)
public class OrderService
{
public Order findById(Long id)
{
// ...
}
}
public class OrderService {
// 1. 常量定义
private static final int DEFAULT_PAGE_SIZE = 20;
// 2. 依赖注入
private final OrderRepository orderRepository;
private final EventPublisher eventPublisher;
// 3. 构造函数
public OrderService(OrderRepository orderRepository, EventPublisher eventPublisher) {
this.orderRepository = orderRepository;
this.eventPublisher = eventPublisher;
}
// 4. 公共方法(按业务逻辑分组)
// ===== 查询方法 =====
public Order findById(Long id) { }
public List<Order> listByStatus(OrderStatus status) { }
// ===== 命令方法 =====
public Order createOrder(CreateOrderRequest request) { }
public void cancelOrder(Long orderId) { }
// 5. 私有方法
private void validateRequest(CreateOrderRequest request) { }
private Order buildOrder(CreateOrderRequest request) { }
}
// ✅ 合理换行(每行不超过 120 字符)
public Order createOrder(
String merchantId,
String productId,
BigDecimal amount,
String currency,
String notifyUrl
) {
// ...
}
// 链式调用换行
return orderRepository.findById(orderId)
.map(order -> {
order.cancel();
return orderRepository.save(order);
})
.orElseThrow(() -> new OrderNotFoundException(orderId));
| 场景 | 注释类型 | 示例 |
|---|---|---|
| 复杂业务规则 | 行内/块注释 | 解释为什么这样计算手续费 |
| 非直观代码 | 行内注释 | 解释位运算逻辑 |
| API 接口 | Javadoc | 参数说明、返回值、异常 |
| 临时方案 | TODO/FIXME | 标记需要后续优化的代码 |
| 特殊处理 | 块注释 | 解释兼容旧系统的特殊逻辑 |
// ❌ 坏的注释:重复代码已经表达的信息
// 设置订单状态为已支付
order.setStatus(OrderStatus.PAID);
// ✅ 好的注释:解释背后的业务原因
// 支付成功后立即更新状态,避免并发场景下重复通知
// 注意:这里不发送消息,由支付回调统一处理
order.setStatus(OrderStatus.PAID);
// ❌ 坏的注释:解释"怎么做"(代码已经说明了)
// 遍历订单列表,找到状态为待支付的订单
for (Order order : orders) {
if (order.getStatus() == OrderStatus.PENDING) {
// ...
}
}
// ✅ 好的注释:解释"为什么这么做"
// 只处理待支付状态的订单,其他状态可能正在异步处理中
// 参见:https://wiki.company.com/order-state-machine
for (Order order : orders) {
if (order.getStatus() == OrderStatus.PENDING) {
// ...
}
}
// TODO(hugo): 2024-06-01 支持批量取消,当前实现是单条循环,性能较差
// 相关需求:REQ-2024-0892
public void cancelOrders(List<Long> orderIds) {
for (Long orderId : orderIds) {
cancelOrder(orderId);
}
}
// FIXME: 并发场景下存在竞态条件,需要加分布式锁
// Issue: https://jira.company.com/BUG-1234
public void deductStock(Long productId, int quantity) {
int current = stockRepository.get(productId);
stockRepository.set(productId, current - quantity);
}
// 业务异常(需要前端展示给用户)
public class BusinessException extends RuntimeException {
private final String errorCode;
private final String userMessage; // 用户友好的提示
public BusinessException(String errorCode, String userMessage) {
super(userMessage);
this.errorCode = errorCode;
this.userMessage = userMessage;
}
}
// 系统异常(内部错误,不暴露给用户)
public class SystemException extends RuntimeException {
private final String traceId;
public SystemException(String message, Throwable cause) {
super(message, cause);
this.traceId = MDC.get("traceId");
}
}
// 具体业务异常
public class OrderNotFoundException extends BusinessException {
public OrderNotFoundException(Long orderId) {
super("ORDER_NOT_FOUND", "订单不存在或已删除");
}
}
public class InsufficientBalanceException extends BusinessException {
public InsufficientBalanceException(BigDecimal required, BigDecimal actual) {
super("INSUFFICIENT_BALANCE",
String.format("余额不足,需要 %.2f,当前 %.2f", required, actual));
}
}
// ❌ 吞掉异常,问题被隐藏
public void processPayment(PaymentRequest request) {
try {
paymentGateway.charge(request);
} catch (Exception e) {
// 什么都不做,支付失败被静默忽略
}
}
// ✅ 正确处理
public void processPayment(PaymentRequest request) {
try {
paymentGateway.charge(request);
} catch (PaymentException e) {
// 记录详细错误信息
log.error("Payment failed for order: {}, error: {}",
request.getOrderId(), e.getMessage(), e);
// 转换为业务异常向上抛
throw new PaymentFailedException(request.getOrderId(), e);
} catch (Exception e) {
// 未知异常,记录并包装
log.error("Unexpected error during payment: {}", request.getOrderId(), e);
throw new SystemException("支付系统异常", e);
}
}
// ❌ 用异常控制正常流程
public Order findOrder(Long orderId) {
try {
return orderRepository.findById(orderId);
} catch (EmptyResultDataAccessException e) {
return null; // 查询不到是正常情况,不应该用异常
}
}
// ✅ 使用 Optional 或空对象模式
public Optional<Order> findOrder(Long orderId) {
return Optional.ofNullable(orderRepository.findById(orderId));
}
// 或者返回空对象
public Order findOrder(Long orderId) {
Order order = orderRepository.findById(orderId);
return order != null ? order : Order.EMPTY;
}
// ❌ 缺乏上下文
throw new RuntimeException("Failed");
// ✅ 包含业务上下文
throw new OrderNotFoundException(orderId);
// 输出:OrderNotFoundException: 订单不存在或已删除 [orderId=12345]
// ✅ 包含技术上下文
throw new DatabaseException(
String.format("查询订单失败 [SQL=%s, params=%s, elapsed=%dms]",
sql, params, elapsed)
);
// ERROR:系统异常,需要立即处理
catch (SQLException e) {
log.error("[{}] 数据库查询失败 | SQL: {} | 原因: {}",
traceId, sql, e.getMessage(), e);
}
// WARN:业务异常,需要关注但不一定立即处理
catch (InsufficientBalanceException e) {
log.warn("[{}] 用户余额不足 | userId: {} | required: {} | actual: {}",
traceId, userId, required, actual);
}
// INFO:正常业务情况
catch (OrderAlreadyPaidException e) {
log.info("[{}] 订单已支付,跳过重复处理 | orderId: {}",
traceId, orderId);
}
| 级别 | 使用场景 | 示例 | 生产环境处理 |
|---|---|---|---|
| ERROR | 系统级错误,需要人工介入 | 数据库连接失败、消息队列宕机 | 立即告警 |
| WARN | 业务异常或潜在问题 | 余额不足、参数校验失败、超时 | 每日汇总告警 |
| INFO | 关键业务节点记录 | 订单创建、支付成功、退款完成 | 保留7-30天 |
| DEBUG | 详细调试信息 | SQL语句、请求参数、中间状态 | 开发环境开启 |
| TRACE | 最详细的跟踪 | 方法进入/退出、变量值 | 极少使用 |
// ❌ 自由格式,难以解析
log.info("Order created: " + orderId + " for user: " + userId + " amount: " + amount);
// ✅ 结构化格式,便于日志系统解析和检索
log.info("[{}] Order created | orderId={} | userId={} | amount={} | currency={} | merchantId={}",
traceId, orderId, userId, amount, currency, merchantId);
// 输出:
// [2024-01-15 10:23:45] [abc-123-def] Order created | orderId=10001 | userId=5001 | amount=99.99 | currency=CNY | merchantId=M001
// ❌ 明文记录敏感信息
log.info("User login: phone={}, password={}", phone, password);
// ✅ 脱敏处理
log.info("[{}] User login | phone={} | result={}",
traceId, maskPhone(phone), "SUCCESS");
// 工具方法
public static String maskPhone(String phone) {
if (phone == null || phone.length() < 7) return phone;
return phone.substring(0, 3) + "****" + phone.substring(7);
}
public static String maskCardNo(String cardNo) {
if (cardNo == null || cardNo.length() < 8) return cardNo;
return cardNo.substring(0, 4) + " **** **** " + cardNo.substring(cardNo.length() - 4);
}
安全红线:密码、Token、密钥、身份证号、银行卡号等绝对禁止以明文形式输出到日志。违反此规定可能导致严重的安全事件。
// ❌ 非线程安全的单例(懒加载)
public class ConfigManager {
private static ConfigManager instance;
public static ConfigManager getInstance() {
if (instance == null) {
instance = new ConfigManager(); // 竞态条件
}
return instance;
}
}
// ✅ 正确的单例模式
public class ConfigManager {
private static class Holder {
static final ConfigManager INSTANCE = new ConfigManager();
}
public static ConfigManager getInstance() {
return Holder.INSTANCE;
}
}
// 或者使用枚举(Effective Java 推荐)
public enum ConfigManager {
INSTANCE;
// ...
}
| 场景 | 推荐 | 避免使用 |
|---|---|---|
| 高并发读、极少写 | CopyOnWriteArrayList |
Vector |
| 高并发读写 | ConcurrentHashMap |
Hashtable, Collections.synchronizedMap |
| 队列(生产者-消费者) | LinkedBlockingQueue |
手动同步的 LinkedList |
| 计数 | LongAdder, AtomicLong |
volatile long + 手动同步 |
// ✅ 使用私有 final 对象作为锁
public class OrderService {
private final Object lock = new Object();
public void process(Long orderId) {
synchronized (lock) {
// 处理逻辑
}
}
}
// ✅ 使用 ReentrantLock 提供更灵活的控制
public class OrderService {
private final ReentrantLock lock = new ReentrantLock();
public void process(Long orderId) {
if (lock.tryLock(5, TimeUnit.SECONDS)) {
try {
// 处理逻辑
} finally {
lock.unlock(); // 必须在 finally 中释放
}
} else {
throw new ConcurrentProcessingException("获取锁超时");
}
}
}
// ❌ 不要使用可变对象作为锁
public class BadExample {
private String lock = "lock"; // String 可能被 intern,导致意外共享
public void process() {
synchronized (lock) { // 危险!
}
}
}
-- ✅ 使用有意义的别名
SELECT
o.id AS order_id,
o.order_no,
o.amount,
o.currency,
o.status,
o.created_at,
m.id AS merchant_id,
m.name AS merchant_name
FROM orders o
INNER JOIN merchants m ON o.merchant_id = m.id
WHERE o.status = 'PENDING'
AND o.created_at >= '2024-01-01'
ORDER BY o.created_at DESC
LIMIT 100;
-- ❌ 避免 SELECT *
SELECT * FROM orders WHERE status = 'PENDING';
-- ✅ 明确指定需要的列
SELECT id, order_no, amount, status FROM orders WHERE status = 'PENDING';
// ❌ 字符串拼接,存在 SQL 注入风险
String sql = "SELECT * FROM orders WHERE order_no = '" + orderNo + "'";
// ✅ 使用参数化查询
String sql = "SELECT * FROM orders WHERE order_no = ?";
jdbcTemplate.query(sql, new OrderMapper(), orderNo);
// ✅ 使用 ORM 框架
@Query("SELECT o FROM Order o WHERE o.orderNo = :orderNo")
Optional<Order> findByOrderNo(@Param("orderNo") String orderNo);
// ✅ 声明式事务(推荐)
@Service
public class OrderService {
@Transactional(rollbackFor = Exception.class)
public Order createOrder(CreateOrderRequest request) {
// 1. 保存订单
Order order = orderRepository.save(buildOrder(request));
// 2. 扣减库存
stockService.deduct(order.getProductId(), order.getQuantity());
// 3. 记录日志
operationLog.record("ORDER_CREATED", order);
// 任何步骤失败都会回滚
return order;
}
}
// ❌ 事务边界过大
@Transactional
public void dailySettlement() {
// 处理 10 万笔订单,事务持有时间太长
List<Order> orders = orderRepository.findAll();
for (Order order : orders) {
process(order); // 事务持续扩大
}
}
// ✅ 批量处理,控制事务粒度
public void dailySettlement() {
List<Order> orders = orderRepository.findAll();
for (List<Order> batch : Lists.partition(orders, 100)) {
processBatch(batch); // 每 100 笔一个事务
}
}
@Transactional(rollbackFor = Exception.class)
public void processBatch(List<Order> batch) {
for (Order order : batch) {
process(order);
}
}
@Test
public void should_calculate_correct_fee_for_vip_user() {
// Given: 准备测试数据
User vipUser = User.builder()
.id(1L)
.type(UserType.VIP)
.level(3)
.build();
BigDecimal amount = new BigDecimal("1000.00");
// When: 执行被测方法
BigDecimal fee = feeCalculator.calculate(vipUser, amount);
// Then: 验证结果
assertThat(fee).isEqualByComparingTo(new BigDecimal("5.00")); // VIP 费率 0.5%
}
@Test
public void should_throw_exception_when_amount_is_negative() {
// Given
User user = User.builder().id(1L).build();
BigDecimal negativeAmount = new BigDecimal("-100");
// When / Then
assertThatThrownBy(() -> feeCalculator.calculate(user, negativeAmount))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("金额不能为负数");
}
| 场景 | 命名模式 | 示例 |
|---|---|---|
| 正常流程 | should_xxx_when_yyy |
should_create_order_when_request_valid |
| 异常流程 | should_throw_xxx_when_yyy |
should_throw_exception_when_balance_insufficient |
| 边界条件 | should_xxx_when_yyy_at_boundary |
should_return_zero_fee_when_amount_is_zero |
| 并发场景 | should_xxx_concurrently |
should_deduct_stock_correctly_concurrently |
| 层级 | 覆盖率要求 | 重点关注 |
|---|---|---|
| 单元测试 | >= 80% | 核心业务逻辑、复杂计算、状态转换 |
| 集成测试 | 核心流程全覆盖 | 数据库交互、外部服务调用、消息队列 |
| 端到端测试 | 主路径覆盖 | 关键用户场景、支付流程、退款流程 |
Hugo 的经验:代码审查不是挑刺,而是共同提升代码质量的过程。
Do:
Don't:
编码规范不是一成不变的,应该随着团队成长和技术发展持续演进:
| 工具类型 | 推荐工具 | 用途 |
|---|---|---|
| 代码格式化 | Spotless, Google Java Format | 自动统一代码格式 |
| 静态分析 | SonarQube, SpotBugs | 发现潜在缺陷、代码异味 |
| 依赖检查 | OWASP Dependency Check | 发现已知漏洞的依赖 |
| 测试覆盖 | JaCoCo | 统计测试覆盖率 |
| 提交检查 | Husky, Commitlint | 规范提交信息 |
包名:com.company.project.module.layer(全小写)
类名:OrderService(名词,首字母大写)
接口名:OrderRepository(名词或形容词)
方法名:findById / createOrder(动词短语,首字母小写)
常量:MAX_RETRY_COUNT(全大写,下划线分隔)
变量:orderItems(驼峰式)
布尔:isActive / hasRefund(肯定语气)
缩进:4 空格(或团队统一)
括号:K&R 风格
行宽:120 字符
空行:逻辑分组之间
注释:解释"为什么",而非"是什么"
不要吞异常
不要用异常控制流程
异常信息要包含上下文
区分业务异常和系统异常
敏感信息不要出现在异常消息中
文档信息