IMOM技术实现部
1. 前言
1.1. 目的
本规范旨在为后端开发团队提供统一的开发标准和指导,确保代码质量的一致性、可读性和可 维护性。 通过规范化的开发流程和编码标准,减少因代码风格不一致、架构设计混乱等问题 带来的沟通成本和开发成本,从而提升开发效率,减少潜在的安全漏洞和维护风险。
1.2. 范围
本规范适用于所有参与后端开发的开发人员、代码审查人员、项目管理者以及其他相关技术人 员,涵盖从编码风格、项目结构、接口设计、安全规范到性能优化等多个方面。在项目的整个 生命周期内,包括开发、测试、上线、维护等环节均应严格遵循本规范。
1.3. 益处
● 提高代码质量:统一的编码规范减少低质量代码的产生,提高代码的可读性和可维护性。
● 保障系统安全:通过规范化的安全处理和数据保护措施,避免常见的安全漏洞,确保系统 的安全性。
● 提升开发效率:标准化的项目结构和清晰的编码风格使团队成员能够快速上手,减少沟通 成本。
● 便于代码审查和协作:代码审查更加高效,通过一致的编码规范降低了审查难度,增强了 团队协作效果。
● 支持系统扩展与升级:良好的代码结构和模块化设计便于系统后续的扩展与升级,降低了 维护成本。
2. 工程架构
2.1. 目录结构
- 【强制】工程根目录mbm-mom-[服务名]
正例:
mbm-mom-mpm/
- 【推荐】子模块命名与结构
- [服务名]-api/
说明:API 子模块,负责暴露服务接口,进行外部系统的交互
- [服务名]-biz/
说明:业务逻辑层模块,处理具体的业务逻辑和规则
正例:
├── mbm-mom-mpm/
├── mpm-api //API接口
├── mpm-biz //业务逻辑实现
- 【强制】包名结构
正例:
├── mbm-mom-mpm/
├── mpm-api
├──com.sie.mbm.mom.mpm
├──com.sie.mbm.mom.mpm.product
├──com.sie.mbm.mom.mpm.product.dto //传输对象
...
├── mpm-biz
├──com.sie.mbm.mom.mpm
├──com.sie.mbm.mom.mpm.product //业务模块-产品
├──com.sie.mbm.mom.mpm.controller //控制器
├──com.sie.mbm.mom.mpm.convert //对象映射转换
├──com.sie.mbm.mom.mpm.constants //常量
├──com.sie.mbm.mom.mpm.entity //实体
├──com.sie.mbm.mom.mpm.enums//枚举
├──com.sie.mbm.mom.mpm.mapper//持久层
├──com.sie.mbm.mom.mpm.service//业务逻辑服务
├──com.sie.mbm.mom.mpm.process //业务模块-工艺
├──com.sie.mbm.mom.mpm.controller //控制器
├──com.sie.mbm.mom.mpm.convert //对象映射转换
....
2.2. 应用分层
- 【强制】图中默认上层依赖于下层,箭头数据流关系,如:开放接口层可以依赖于 Web 层(Controller),Controller依赖于 Service 层,依此类推。
• 开放接口层:暴露成 RPC 接口;通过 Web 封装成 http 接口;网关控制层等。
• 终端显示层:各个端的显示的层。当前PC端,移动端展示等。
• Controller层:主要是对访问控制进行转发,各类基本参数校验等,不做业务处理。
• Service 层:相对具体的业务逻辑服务层。
• SDK层:通用业务处理层,它有如下特征:
1) 对第三方平台封装的层,预处理返回结果及转化异常信息。
2) 对 Service 层通用能力的下沉,如缓存方案、中间件通用处理。
3) 与 DAO 层交互,对多个 DAO 的组合复用。
• DAO 层:数据访问层,与底层 iDME\PostgreSQL 等进行数据交互。
• 外部接口或第三方平台:包括MBM等产品 RPC 开放接口,基础平台,其它公司的接口。
-
【推荐】分层异常处理规约
在 DAO、SDK层,产生的异常类型有很多,无法用细粒度的异常进 行 catch,使用 catch(Exception e)方式,并 DmeAssert.throwException(TokenExpireEnum.PARSE);不需要打印日志,因为日志在Service 层一定需要捕获并打印到日志文件中去,如果同台服务器再打日志, 浪费性能和存储。在 Service 层出现异常时,必须记录出错日志到磁盘,尽可能带上参数信息, 相当于保护案发现场。
Web 层绝不应该继续往上抛异常,因为已经处于顶层,如果意识到这个异常将导致页面无法正常渲染,那么就应该直接跳转到友好错误页面, 尽量加上友好的错误提示信息。开放接口层要将异常处理成错误码和错误信息方式返回。
2.3. 模型分层
- DO(Data Object):此对象与数据库表结构一一对应,通过 DAO 层向上传输数据源对象。
说明:放到entity包中
正例:
@Table("SieEmployee")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@NoArgsConstructor
@AllArgsConstructor
public class Employee extends BaseEntity<Employee> {
@Schema(description = "主键ID")
private Long id;
}
- DTO(Data Transfer Object):数据传输对象,Service 或 API 向外传输的对象。
说明:放到dto包中
2.4. 库依赖
- 【强制】二方库定义 GAV 遵从以下规则
- GroupID 格式:com.sie.业务线 [.子业务线],最多 4 级
正例:
com.sie.mbm.mom
- ArtifactID 格式:产品线名-模块名-子模块。语义不重复不遗漏
正例:
mbm-mom-mpm
- 【强制】二方库版本号命名方式:主版本号.次版本号.修订号
- 主版本号:产品方向改变,或者大规模 API 不兼容,或者架构不兼容升级。
- 次版本号:保持相对兼容性,增加主要功能特性,影响范围极小的 API 不兼容修改。
- 修订号:保持完全兼容性,修复 BUG、新增次要功能特性等。
说明:注意起始版本号必须为:1.0.0,而不是 0.0.1。
反例:
版本号从 1.0.0.0 开始,升级成 1.0.0.64,// ❌完全失去版本的语义信息。
- 【强制】线上应用不要依赖 SNAPSHOT 版本(安全包除外)
说明:不依赖 SNAPSHOT 版本是保证应用发布的幂等性,使 RELEASE 版本号有延续性,且版本号不允许覆盖升级。
- 【强制】所有 pom 文件中的依赖声明放在
语句块中,所有版本仲裁放在最顶级 语句块中。
说明:
- 【强制】引入新的第三方库需进行评审,确保功能适配性、兼容性、安全性和可维护性。评审通过后,需遵循集中管理、版本控制及封装使用等。
- 【推荐】二方库不要有配置项,最低限度不要再增加配置项
3. 编程规范
3.1. 命名风格
- 【强制】所有命名(变量、方法、类、常量等)均不能以下划线 _ 或美元符号 $ 开头或结尾
反例:
private int _count; // ❌ 不能以下划线开头
private int count_; // ❌ 不能以下划线结尾
private String $name; // ❌ 不能以美元符号开头
private String name$; // ❌ 不能以美元符号结尾
正例:
private int count; // ✅ 正确
private String name; // ✅ 正确
- 【强制】所有(变量、方法、类、常量等)的命名必须使用标准的英文单词,严禁使用拼音与英文混合的方式,更不允许使用中文命名。
反例:
private String huoQuUserInfo(); // ❌ 拼音 + 英文混合
private int shuliang; // ❌ 纯拼音
private String 用户名; // ❌ 直接使用中文
正例:
private String getUserInfo(); // ✅ 正确,标准英文命名
private int quantity; // ✅ 正确,标准英文命名
- 【强制】类名必须使用 UpperCamelCase(首字母大写,每个单词的首字母都大写)。但以下情况例外:DO、DTO、VO等通用术语可以全部大写。
反例:
public class userManager { } // ❌ 错误:首字母应大写
public class user_manager { } // ❌ 错误:不能使用下划线
public class User_manager { } // ❌ 错误:单词首字母应大写
public class usermanager { } // ❌ 错误:应使用 CamelCase
正例:
public class UserManager { } // ✅ 正确,采用 UpperCamelCase
public class UserDTO { } // ✅ 正确,DTO 全部大写
- 【强制】方法名、参数名、成员变量、局部变量必须统一使用 lowerCamelCase(小驼峰命名法),即首字母小写,后续单词首字母大写。
反例:
private String UserName; // ❌ 错误:首字母不应大写
private String user_name; // ❌ 错误:不能使用下划线
private String username; // ❌ 不推荐:可读性差
public void GetUserInfo() { } // ❌ 错误:方法首字母不应大写
public void get_user_info() { } // ❌ 错误:不能使用下划线
- 【强制】常量命名全部大写,单词间用下划线隔开,力求语义表达完整清楚,不要嫌名字长。
反例:
public static final int maxCount = 100; // ❌ 错误:未使用全大写
public static final String defaultEncoding = "UTF-8"; // ❌ 错误:未使用全大写
public static final double Pi = 3.14159; // ❌ 错误:未使用全大写
public static final String DB = "MySQL"; // ❌ 错误:名称过短,缺乏语义信息
正例:
public static final int MAX_RETRY_COUNT = 5; // ✅ 正确,清晰表达了最大重试次数
public static final String DEFAULT_ENCODING = "UTF-8"; // ✅ 正确,清晰表达了默认编码
public static final double PI = 3.14159; // ✅ 正确,数学常量
public static final String DATABASE_TYPE = "MySQL"; // ✅ 正确,清晰表达数据库类型
- 【强制】DTO类中的任何布尔类型变量,不要加 is 前缀,否则可能导致某些框架(如 MyBatis、Jackson)解析和序列化错误。
反例:
public class User {
private Boolean isActive; // ❌ 错误:使用了 is 前缀
private Boolean isDeleted; // ❌ 错误:使用了 is 前缀
public Boolean isActive() { // ❌ 错误:getter 方法会导致序列化问题
return isActive;
}
public void setActive(Boolean isActive) {
this.isActive = isActive;
}
}
正例:
public class User {
private Boolean active; // ✅ 正确:布尔变量直接命名
public Boolean isActive() { // ✅ getter 方法符合规范
return active;
}
public void setActive(boolean active) {
this.active = active;
}
}
- 【强制】包名统一使用小写,点分隔符之间有且仅有一个自然语义的英语单词。包名统一使用单数形式,但是类名如果有复数含义,类名可以使用复数形式。
反例:
package com.Sie.Project.Modules.Users; // ❌ 错误:大小写混用、复数形式
package com.sie.utils.helper_classes; // ❌ 错误:下划线不允许
package com.sie.ordersManagement; // ❌ 错误:大小写混用
package com.sie.orderprocess; // ❌ 错误:单词无语义,应为 order.processing
正例:
package com.sie.mom.module.user; // ✅ 规范:单数形式
- 【强制】杜绝完全不规范的缩写,确保代码可读性,避免让人“望文不知义”。
反例:
String usrNm = "John Doe"; // ❌ usrNm -> userName,缩写不清晰
String cfg = "config.xml"; // ❌ cfg -> config,避免过度缩写
int cstAmt = 100; // ❌ cstAmt -> costAmount,容易混淆
List<Cst> cstList = new ArrayList<>(); // ❌ Cst -> Customer,无法直观理解
- 【参考】方法命名规约:
- 获取单个对象的方法用 get 做前缀。
- 获取多个对象的方法用 list 做前缀,复数结尾,如:listObjects。
- 获取统计值的方法用 count 做前缀。
- 插入的方法用 save/insert 做前缀。
- 删除的方法用 remove/delete 做前缀。
- 修改的方法用 update 做前缀。
- 批量操作 batch做前缀
正例:
User getUserById(Long userId); // ✅ 获取
List<User> listUsers(); // ✅ 集合
Integer countUsers(); // ✅ 统计
Long saveUser(UserDTO userDTO); // ✅ 保存
List<Long> saveUsers(List<UserDTO> userListDTO); //批量保存
Integer deleteUserById(Long userId); // ✅ 删除
Boolean updateUser(UserDTO userDTO); // ✅ 更新规范
Integer updateUsers(List<UserDTO> userListDTO); //✅ 批量更新用户信息,返回更新成功的数量
Integer deleteUsersByIds(List<Long> userIds); // ✅ 批量删除用户,返回删除成功的数量
Integer saveOrUpdateUsers(List<UserDTO> userDTOList); //✅ 批量新增或更新,返回操作成功的数量
BatchOperationResult batchProcessUsers(BatchUserDTO batchUserDTO); //✅ 统一处理新增、更新、删除,返回操作结果
3.2. 注解规约
- 【强制】类、类属性、类方法的注释必须使用 Javadoc 规范,使用/**内容*/格式,不得使用 // xxx 方式。
说明:在 IDE 编辑窗口中,Javadoc 方式会提示相关注释,生成 Javadoc 可以正确输出相应注释;在 IDE 中,工程调用方法时,不进入方法即可悬浮提示方法、参数、返回值的意义,提高阅读效率。
参考Javadoc(文档注释)详解:链接
- 【强制】提供包级别的注释需要使用 package-info.java 文件
正例:
/**
* 该包包含用户管理的相关类。
* <p>
* 主要功能包括:
* - 用户注册、登录
* - 权限管理
* - 个人信息管理
* </p>
*
* @author 中文名称带数字
* @date 2024/08/08
* @description: 简介说明
**/
package com.example.usermanagement;
import com.example.annotations.PackageAnnotation;
- 【强制】所有的类都必须添加创建者、说明和创建日期。
说明:在设置模板时,注意 IDEA 的@author 为`${USER}`,而 eclipse 的@author 为`${user}`,大小写有 区别,而日期的设置统一为 yyyy/MM/dd 的格式。
在 IDEA 中配置 文件模板 (File Templates) 参考:
#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")
package ${PACKAGE_NAME};
#end
/**
*
* @author ${USER}
* @date ${YEAR}/${MONTH}/${DAY}
* @description
*/
public class ${NAME} {
}
正例:
/**
* 包装规则
*
* @author 中文名称(有数字则带上)
* @date 2024/08/08
* @description: 包装规则描述
**/
public class PackRule {
/**
* 产品编码
*/
private String partNo;
}
- 【强制】方法注释尽量简洁,突出关键点; 参数、返回值、异常等信息必须补充完整,保持一致的格式,便于团队阅读和维护
正例:
/**
* 简要描述方法的作用
*
* @param param1 参数1的说明(无入参则不写)
* @param param2 参数2的说明
* @return 返回值说明(void则不写)
* @throws ExceptionType 异常说明
* @deprecated 如果方法已废弃,提供替代方案
*/
public ReturnType methodName(Type param1, Type param2) throws ExceptionType {
// 方法实现
}
- 【强制】方法内部单行注释,在被注释语句上方另起一行,使用//注释。方法内部多行注释使 用/* */注释,注意与代码对齐。
正例:
public void processOrder() {
// 计算折扣
int discount = 10;
}
public void generateReport() {
/*
* 生成报表的逻辑,包含:
* 1. 查询数据库
* 2. 处理数据
* 3. 格式化报表输出
*/
reportService.create();
}
- 【强制】所有的枚举类型字段必须要有注释,说明每个数据项的用途。
正例:
/**
* 订单状态枚举
*
* @author 中文名称带数字
* @date 2024/08/08
* @description: 订单状态枚举描述
*/
public enum OrderStatus {
/** 订单已创建,等待支付 */
PENDING_PAYMENT,
/** 订单已支付,等待发货 */
PAID,
/** 订单已发货,等待收货 */
SHIPPED,
/** 订单已完成,交易成功 */
COMPLETED,
/** 订单已取消 */
CANCELED;
}
- 【强制】代码修改的同时,注释也要进行相应的修改,尤其是参数、返回值、异常、核心逻辑 等的修改。
说明:代码与注释更新不同步,就像路网与导航软件更新不同步一样,如果导航软件严重滞后,就失去了导航的意义。
正例:
/**
* 获取用户年龄
*
* @author 中文名称(有数字则带上)
* @date 2024/08/08
* @param userId 用户ID
* @return String 用户年龄字符串
*/
public String getUserAge(String userId) {
return "25";
}
反例:注释仍然写着返回 int,但方法已经改为 String,容易误导调用者。
/**
* 获取用户年龄
*
* @author 中文名称(有数字则带上)
* @date 2024/08/08
* @param userId 用户ID
* @return int 用户年龄
*/
public String getUserAge(String userId) {
return "25"; // 这里返回的是 String,但注释还写着 int!
}
- 【强制】复杂逻辑的注释
说明:对于复杂的算法或业务逻辑,注释应该解释清楚该逻辑的目的、思路以及具体步骤。
正例:
// 计算折扣时,使用了逐个商品的折扣率,再加上全局折扣率进行计算
// 折扣公式:商品价格 * 商品折扣 * 全局折扣
double finalPrice = item.getPrice() * item.getDiscount() * globalDiscount;
/**
* 计算购物车中商品的总价。
* 遍历每个商品,检查商品是否有折扣,应用折扣后累加总价。
* 如果有优惠券,进一步减免总价。
*
* @param cartItems 购物车中的商品列表
* @param coupon 优惠券信息(如果有)
* @return 购物车的总价
*/
public double calculateTotalPrice(List<Item> cartItems, Coupon coupon) {
//业务逻辑
}
反例:
// ❌没有注释解释为什么某些代码片段这么做,代码的目的和思路不明确,其他开发者阅读时不容易理解。
public double calculateTotalPrice(List<Item> cartItems, Coupon coupon) {
double total = 0.0;
for (Item item : cartItems) {
// 计算商品价格
total += item.getPrice();
}
// 应用优惠券
if (coupon != null) {
total -= coupon.getDiscount();
}
return total;
}
- 【参考】特殊注释标记,请注明标记人与标记时间。注意及时处理这些标记,通过标记扫描, 经常清理此类标记。线上故障有时候就是来源于这些标记处的代码。
1)待办事宜(TODO):(标记人,标记时间,[预计处理时间]) 表示需要实现,但目前还未实现的功能。这实际上是一个 Javadoc 的标签,目前的 Javadoc 还没有实现,但已经被广泛使用。只能应用于类,接口和方法(因为它是一个 Javadoc 标签)。
正例:
/**
* 处理用户登录逻辑
* TODO (张三, 2025-02-25, [2025-03-01]) 优化登录失败的错误提示信息
*/
public void login(String username, String password) {
// 登录逻辑待优化
}
2) 错误,不能工作(FIXME):(标记人,标记时间,[预计处理时间]) 在注释中用 FIXME 标记某代码是错误的,而且不能工作,需要及时纠正的情况。
正例:
/**
* 计算折扣价格
* FIXME (张三, 2025-02-25, [2025-02-27]) 折扣计算逻辑错误,导致负值
*/
public double calculateDiscount(double price, double discount) {
return price - price * discount; // 可能导致负值,需修正
}
3.3. 常量定义
- 【强制】不允许任何魔法值(即未经预先定义的常量)直接出现在代码中。
正例:
public class CacheKeys {
/**
* 淘宝交易缓存的 Key 前缀
*/
public static final String TAOBAO_TRADE_KEY_PREFIX = "Id#taobao_";
}
public class CacheService {
public void saveToCache(String tradeId, Object value) {
String key = CacheKeys.TAOBAO_TRADE_KEY_PREFIX + tradeId;
cache.put(key, value);
}
}
public class OrderStatus {
/**
* 订单待处理状态
*/
public static final int STATUS_PENDING = 3;
}
public class OrderService {
public void processOrder(int status) {
if (status == OrderStatus.STATUS_PENDING) {
System.out.println("订单待处理...");
// 处理逻辑
}
}
}
反例:
public void saveToCache(String tradeId, Object value) {
String key = "Id#taobao_" + tradeId; // ❌ 硬编码,后续修改不便
cache.put(key, value);
}
public void processOrder(int status) {
if (status == 3) { // ❌ 魔法值,阅读代码时无法直接理解 3 代表什么
System.out.println("订单待处理...");
}
}
- 【强制】常量必须使用 static final 关键字定义,命名应使用大写字母+下划线分隔,并加上注解
正例:
/**
* 最大重试次数,表示操作失败时最多可以重试的次数。
* 例如:网络请求失败时,最多允许重试 5 次。
*/
public static final int MAX_RETRY_COUNT = 5;
/**
* 默认编码格式,用于处理字符串编码,例如文件读取、网络传输等。
* 常见值:UTF-8、GBK、ISO-8859-1。
*/
public static final String DEFAULT_ENCODING = "UTF-8";
反例:
//❌ 没有注解
public final int maxRetryCount = 5; // ❌ 没有 static,无法共享
public static String defaultEncoding = "UTF-8"; // ❌ 没有 final,容易被修改
- 【强制】在 long 或者 Long 赋值时,数值后使用大写的 L,不能是小写的 l,小写容易跟数字 混淆,造成误解。
正例:
Long value = 2L; // ✅ 大写 L,清晰易读
long maxLimit = 10000000000L; // ✅ 正确,避免误读
反例:
Long value = 2l; // ❌ 小写 l,容易误认为 21
long maxLimit = 10000000000l; // ❌ 看起来像 100000000001
3.4. 代码格式
- 【强制】缩进与代码块,采用 4 个空格缩进(不要使用 Tab);大括号 {} 的写法采用 "行尾风格",即左大括号 { 与语句在同一行,右大括号 } 另起一行。
正例:
public class Example {
public void method() {
if (true) {
System.out.println("遵循代码缩进规范");
} else {
System.out.println("保持一致");
}
}
}
- 【强制】代码换行(分屏换行),单行字符数限制不超过 120 个(一屏能显示),超过应进行合理换行。
- 【强制】IDE 的 text file encoding 设置为 UTF-8; IDE 中文件的换行符使用 Unix 格式,不要使用 Windows 格式。
- 【推荐】方法参数换行,当参数较多时,每个参数换行并对齐
正例:
public void doSomething(
String name, int age,
boolean isActive, double score) {
// 方法体
}
反例:
public void doSomething(String name, int age,
boolean isActive, double score) {
// ❌ 第二行缩进不对齐
}
4. 接口规范
4.1. 基本要求
为了实现高质量、可读性、可维护性和可扩展性的RESTful API,应遵循以下设计规范:
- 协议:使用HTTP/HTTPS协议进行通信。
- 请求方法:常用的请求方法有GET、POST、PUT、DELETE等。
- 无状态:应该是无状态的,即它不应该依赖于上一个请求的状态。
- 缓存:应该支持缓存机制,以提高性能和可用性。
- 幂等性:的设计应具有幂等性,即对于同一个请求,无论请求多少次,结果都是一样的。
- 可扩展性:应该具有良好的可扩展性,以便支持未来的新功能和业务场景。
4.2. 类级别路径或全局路径
- 【强制】为该控制器类中的所有方法提供一个公共的前缀路径
说明:@RequestMapping("[版本]/[模块]/[类]"),模块为可选项,模块并非微服务名,如:process 代表工艺模块。
正例:
@RequestMapping("/v1/process/masters") // ✅ 正确
public class MasterController {
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteMaster(@PathVariable Long id) {
//业务逻辑
}
}
反例:
@RequestMapping("/process/masters") // ❌ 错误,没有版本号
@RequestMapping("v1/mes/process/masters") // ❌ 错误,不需要加应用服务名
public class UserController {
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteMaster(@PathVariable Long id) {
//业务逻辑
}
}
public class UserController {
@DeleteMapping("v1/smt/process/masters") // ❌ 错误,没有全局声明
public ResponseEntity<Void> deleteMaster(@PathVariable Long id) {
//业务逻辑
}
}
4.3. 方法与操作
- 【强制】API 的 HTTP 方法对应以下操作
| HTTP 方法 | 操作 | JAVA 注解 | 说明 |
|---|---|---|---|
| GET | 获取资源 | GetMapping | 获取某个资源或者资源列表 |
| POST | 创建资源 | PostMapping | 创建一个新资源,或入参是DTO对象 |
| PUT | 更新资源 | PutMapping | 完全更新某个资源 |
| PATCH | 部分更新资源 | PatchMapping | 更新资源的部分内容(可选) |
| DELETE | 删除资源 | DeleteMapping | 删除某个资源 |
正例:
@RestController
@RequestMapping("/v1/users")
public class UserController {
// 获取所有用户
@GetMapping
public R<List<User>> getUsers() {
return userService.getAllUsers();
}
// 获取特定用户
@GetMapping("/by-user-id")
public R<User> getUser(@RequestParam("id") Long userId) {
return userService.getUserById(userId);
}
// 创建用户
@PostMapping
public User createUser(@Valid @RequestBody UserDTO user) {
return userService.createUser(user);
}
// 更新用户
@PutMapping()
public User updateUser(@Valid @RequestBody UserDTO user) {
return userService.updateUser(user);
}
// 删除用户
@DeleteMapping()
public void deleteUser(@RequestParam("id") Long userId) {
userService.deleteUser(userId);
}
//批量删除用户
@DeleteMapping("/delete-batch-by-ids")
public void deleteBatch(@RequestParam("ids") Collection<Long> ids) {
userService.deleteBatch(ids);
}
}
- 【推荐】传递复杂的 JSON 数据或对象,应使用 POST 或 PUT 方法,并在方法中使用 @RequestBody。
说明:使用 @GetMapping 配合 @RequestBody 作为参数时,存在生成文档参数看不到明细的问题
// 查询用户
@PostMapping("/search")
@Operation( summary = "查询用户", description = "通过请求体传递复杂的查询条件" )
public List<User> searchUsers(@Valid @RequestBody @Parameter(description = "查询条件") SearchCriteria searchCriteria) {
// 查询逻辑
}
4.4. 方法路径规则
- 【强制】路径命名规则,使用分隔单名:中划线 (kebab-case) 全小写URL 路径
正例:
/userProfile, /OrderHistory /order_history ❌ 错误
/user-profile, /order-history ✅ 正确
- 【推荐】URL(宾语)是名词
正例:
// 根据ID获取用户
@GetMapping("/user-by-id") // ✅ 正确,路径中不包含动词,HTTP 方法(GET)表示查询
public ResponseEntity<UserDTO> getUserById(@RequestParam Long id) {
// 处理逻辑
}
反例:
// 错误示例:路径包含动词
@GetMapping("/get-user-by-id") // ❌ 错误,路径中包含了动词 "get"
public ResponseEntity<UserDTO> getUserById(@RequestParam Long id) {
// 处理逻辑
}
4.5. 版本控制
- 【推荐】版号原则说明
主版本号:产品方向改变,或者大规模 API 不兼容,或者架构不兼容升级。
次版本号:保持相对兼容性,增加主要功能特性,影响范围极小的 API 不兼容修改。
修订号:保持完全兼容性,修复 BUG、新增次要功能特性等。
- 【强制】将版本号放入URL中(方便直观)
版本号:v{n}n 代表版本号,分为整形和浮点型
整型:大功能版本发布形式:具有当前版本状态下的所有 API接口,例如:v1,v2
浮点型:为小版本号,只具备补充 api 的功能,其他 api 都默认调用对应大版本号的 api 例如:v1.1 v2.2。
正例:
@RestController
@RequestMapping("/v1/users") // ✅ 正确 在类上定义版本号
public class UserController {
}
4.6. HTTP 状态码
- 【强制】根据不同的情况返回相应的 HTTP 状态码
正例:
| 状态码 | 说明 | 适用场景 |
|---|---|---|
| 200 OK | 请求成功,且返回数据(GET、POST、PUT、DELETE) | 获取数据、更新数据成功 |
| 201 Created | 请求成功,且资源已被创建 | POST 请求成功后返回资源 |
| 204 No Content | 请求成功,但无返回内容 | DELETE 请求成功 |
| 400 Bad Request | 请求无效,客户端错误 | 请求缺少参数、格式错误 |
| 401 Unauthorized | 未授权,认证失败 | 需要认证才能访问 |
| 403 Forbidden | 禁止访问,权限不足 | 无权限访问该资源 |
| 404 Not Found | 请求资源不存在 | 资源未找到 |
| 405 Method Not Allowed | 方法不允许 | 请求方法不支持 |
| 500 Internal Server Error | 服务器错误 | 服务器异常 |
4.7. 接口文档注解
参考:Swagger 3 注解使用指南 链接
5. 异常规范
- 【强制】Java 类库中定义的可以通过预检查方式规避的 RuntimeException 异常不应该通过 catch 的方式来处理,比如:NullPointerException,IndexOutOfBoundsException 等等。
说明:避免 catch 预检查可规避的异常,优先进行 null 判断、索引检查、参数校验。真正不可预见的异常(如 IO 异常、数据库异常),才使用 try-catch 处理。
正例
if (user != null) {
String name = user.getName();
}
if (CollectionUtils.isNotEmpty(list)) {
int value = list.get(5);
}
反例
try {
String name = user.getName(); // 可能抛 NPE
} catch (NullPointerException e) {
logger.error("空指针异常", e); // ❌ 不推荐
}
try {
int length = str.length(); // ❌ 可能抛 NPE
} catch (NullPointerException e) {
logger.error("字符串为空", e);
}
try {
int value = list.get(5); // ❌ 可能越界
} catch (IndexOutOfBoundsException e) {
logger.error("索引越界", e);
}
- 【强制】catch 时请分清稳定代码和非稳定代码,稳定代码指的是无论如何不会出错的代码。 对于非稳定代码的 catch 尽可能进行区分异常类型,再做对应的异常处理。
说明
- 稳定代码 不放 try-catch 里,避免影响异常捕获逻辑。
- 针对非稳定代码,精确 catch 具体异常,不同异常不同处理方式。
- 禁止 catch Exception e 直接打印,会丢失异常类型,影响排查。
反例
try {
log.info("开始执行任务");
String content = new String(Files.readAllBytes(Paths.get("config.txt"))); // 可能抛 IO 异常
int result = 10 / num; // 可能抛 ArithmeticException
processData(content); // 可能抛 NullPointerException
} catch (Exception e) {
log.error("发生异常", e); // ❌ 粗暴捕获,无法区分异常类型
}
正例:
log.info("开始执行任务"); // ✅ 稳定代码,放 try-catch 外
try {
String content = new String(Files.readAllBytes(Paths.get("config.txt"))); // 可能抛 IO 异常
int result = 10 / num; // 可能抛 ArithmeticException
processData(content); // 可能抛 NullPointerException
} catch (IOException e) {
log.error("文件读取失败", e); // ✅ 处理 IO 相关问题
} catch (ArithmeticException e) {
log.error("数学运算错误", e); // ✅ 处理数学异常
} catch (NullPointerException e) {
log.error("空指针异常", e); // ✅ 处理空指针异常
}
- 【强制】捕捕获的异常类型 必须与 抛出的异常类型 完全匹配,或者 捕获异常是抛出异常的父类
说明:如果代码 捕获了错误的异常类型,可能会导致异常无法正确处理,甚至隐藏潜在的 Bug
反例
public void process() {
try {
throw new IOException("IO 发生异常");
} catch (NullPointerException e) { // ❌ 代码无法捕获 IOException
System.out.println("捕获到空指针异常");
}
}
正例
public void process() {
try {
riskyOperation();
} catch (IOException e) { // ✅ 精确匹配,处理 IO 异常
System.out.println("文件读取失败:" + e.getMessage());
} catch (SQLException e) { // ✅ 精确匹配,处理数据库异常
System.out.println("数据库操作失败:" + e.getMessage());
} catch (Exception e) { // ✅ 兜底处理其他异常
System.out.println("发生未知异常:" + e.getMessage());
}
}
public void riskyOperation() throws IOException, SQLException {
// 可能抛出 IOException 或 SQLException
}
- 【强制】捕获异常是为了处理它,不要捕获了却什么都不处理而抛弃之,如果不想处理它,请将该异常抛给它的调用者。最外层的业务使用者,必须处理异常,将其转化为用户可以理解的 内容。
反例
try {
// 数据库操作
String result = databaseService.queryData("someQuery");
} catch (SQLException e) {
// 捕获了异常,但什么都没做,异常被吞掉,最少要写个日志。
}
正例
try {
// 数据库操作
String result = databaseService.queryData("someQuery");
} catch (SQLException e) {
log.error("查询数据时发生错误", e);
// 处理异常:可能是数据库连接问题、查询错误等
throw new BusinessException("数据查询失败,请稍后重试");
}
- 【推荐】方法的返回值可以为 null,不强制返回空集合,或者空对象等,必须添加注释充分说 明什么情况下会返回 null 值。
说明:明确防止 NPE 是调用者的责任。即使被调用方法返回空集合或者空对象,对调用者来说,也并非高枕无忧,必须考虑到远程调用失败、序列化失败、运行时异常等场景返回 null 的情况。
正例
/**
* 获取用户的详细信息
*
* @param userId 用户 ID
* @return 用户的详细信息,如果用户不存在,则返回 null //✅ 正例:返回 null 并做充分说明
*/
public User getUserDetails(String userId) {
if (userId == null || !userExists(userId)) {
return null; // 如果用户不存在,返回 null
}
return findUserById(userId);
}
反例
//❌ 错误示例:返回 null 而不说明
public User getUserDetails(String userId) {
if (userId == null || !userExists(userId)) {
return null;
}
return findUserById(userId);
}
- 【推荐】防止 NPE,注意 NPE 产生的场景
- 自动拆箱可能导致 NPE
反例
public int getAge() {
Integer age = null;
return age; // ❌ 自动拆箱时抛出 NullPointerException
}
正例:
public int getAge() {
Integer age = null;
return Optional.ofNullable(age).orElse(0); // ✅ 推荐使用 Optional 处理
}
- 查询结果可能为 null
反例
User user = userRepository.findById(userId);
String username = user.getName(); // ❌ 如果 user 为空,则调用 getName() 触发 NPE
正例
String username = Optional.ofNullable(userRepository.findById(userId))
.map(User::getName)
.orElse("默认名称"); // ✅ 推荐使用 Optional
- 级联调用可能导致 NPE
反例
String city = user.getAddress().getCity(); // ❌ 如果 user 或 address 为 null,则 NPE
正例
String city = (user != null && user.getAddress() != null) ? user.getAddress().getCity() : "未知";
String city = Optional.ofNullable(user)
.map(User::getAddress)
.map(Address::getCity)
.orElse("未知"); // ✅ 推荐
- 集合中元素可能为 null
反例
List<String> names = new ArrayList<>();
names.add(null);
System.out.println(names.get(0).length()); // ❌ 取值时 NPE
正例
for (String name : names) {
System.out.println(Objects.requireNonNullElse(name, "默认值").length()); // ✅ 确保不为 null
}
6. 日志规范
6.1. 基本要求
- 【强制】应用中不可直接使用日志系统(Log4j、Logback)中的 API,而应依赖使用日志框架(SLF4J)中的 API,使用门面模式的日志框架,有利于维护和各个类的日志处理方式统一。
正例:
//✅ 正确使用@Slf4j注解
@Slf4j
public class HttpInvokerServiceExporterAspect {
public void mypointcut() {
log.info("test");
}
}
- 【强制】在日志输出时,字符串变量之间的拼接使用占位符的方式。
说明:因为 String 字符串的拼接会使用 StringBuilder 的 append()方式,有一定的性能损耗。使用占位符仅
是替换动作,可以有效提升性能。
正例:
logger.info("Processing order: orderId={}, userName={}", orderId, userName);
反例:
// ❌错误示例 1:直接使用 + 进行拼接
logger.info("Processing order: orderId=" + orderId + ", userName=" + userName);
// ❌错误示例 2:字符串拼接会导致性能损耗
logger.info("Processing order: " + String.format("orderId=%s, userName=%s", orderId, userName));
- 【强制】对于 trace/debug/info 级别的日志输出,必须进行日志级别的开关判断。
说明:尽管 debug(参数) 方法内第一行代码 isDisabled(Level.DEBUG_INT) 为真时(如 Log4j、Logback),会直接 return,但参数仍可能发生字符串拼接。此外,debug(getName()) 形式的调用会导致不必要的方法开销。
// 如果判断为真,那么可以输出 trace 和 debug 级别的日志
if (logger.isDebugEnabled()) {
logger.debug("Current ID is: {} and name is: {}", id, getName());
}
- 【强制】生产环境禁止直接使用 System.out 或 System.err 输出日志或使用 e.printStackTrace()打印异常堆栈。
正例:
logger.error("发生异常: {}", e.getMessage(), e);
反例:
//❌日志不可控,生产环境可能无法记录。
System.out.println("系统启动成功");
try {
int result = 10 / 0;
} catch (Exception e) {
e.printStackTrace(); //❌仅在控制台打印异常,不会被日志系统捕获,排查问题困难。
}
- 【强制】异常信息应该包括两类信息:案发现场信息和异常堆栈信息。如果不处理,那么通过 throws 关键字往上抛出。
说明:在异常处理时,必须包含完整的错误上下文,以便于日志排查和问题分析。
格式:【模块名称】 业务关键字段 , 事件描述 详细异常信息
关键日志字段
| 字段 | 说明 | 示例 |
|---|---|---|
| 【模块名称】 | 业务功能模块 | 【订单处理】, 【支付处理】 |
| 业务ID | 关键业务 ID | 订单ID: 123456, 用户ID: 98765 |
| 状态 | 业务状态 | 订单状态: PENDING, 支付状态: FAILED |
| 关键参数 | 影响逻辑的参数 | 支付方式: Alipay, 库存: 10 |
| 时间 | 记录时间点 | 2025-03-04T10:00:00 |
| 异常信息 | 详细异常栈 | NullPointerException: xxx |
正例:
try {
int result = 10 / 0;
} catch (ArithmeticException e) {
logger.info("【支付处理】 订单ID: {} , 支付方式: {} ,交易状态: {} ,处理时间: {}", orderId, paymentMethod, status, LocalDateTime.now());
throw new BusinessException("订单处理异常,ID: " + orderId, e);
}
反例:
try {
int result = 10 / 0;
} catch (ArithmeticException e) {
logger.error("计算错误", e);
// ❌ 没有抛出异常,导致业务流程异常中断
}
- 【强制】日志打印时禁止直接用 JSON 工具将对象转换成 String。
正例:
logger.error("【订单处理失败】订单ID: {}, 用户ID: {}, 错误信息: {}", order.getOrderId(), order.getUserId(), e.getMessage(), e);
反例:
logger.info("订单信息: {}", JSON.toJSONString(order)); // ❌ 禁止使用
logger.info("订单详情: {}", order); // ❌ 不推荐
- 【推荐】谨慎地记录日志。生产环境禁止输出 debug 日志;有选择地输出 info 日志;如果使用 warn 来记录刚上线时的业务行为信息,一定要注意日志输出量的问题,避免把服务器磁盘撑 爆,并记得及时删除这些观察日志。
说明:大量地输出无效日志,不利于系统性能提升,也不利于快速定位错误点。记录日志时请思考:这些日志真的有人看吗?看到这条日志你能做什么?能不能给问题排查带来好处?
- 【推荐】可以使用 warn 日志级别来记录用户输入参数错误的情况,避免用户投诉时,无所适从。如非必要,请不要在此场景打出 error 级别,避免频繁报警。
说明:注意日志输出的级别,error级别只记录系统逻辑出错、异常或者重要的错误信息。
6.2. 日志配置
- 【强制】日志格式
时间 | 级别 | 线程 | 类名 | 方法 | 行号 | 追踪ID | 消息
2025-03-03 12:00:00.123 | INFO | main | com.example.service.UserService | getUser | 45 | traceId=abc123 | 查询用户信息成功
- 【推荐】统一 JSON 格式(方便日志分析)
{
"timestamp": "2025-03-03T12:00:00.123",
"level": "INFO",
"thread": "main",
"logger": "com.example.service.UserService",
"method": "getUser",
"line": 45,
"traceId": "abc123",
"message": "查询用户信息成功"
}
- 【推荐】链路追踪ID
使用 MDC 统一追踪 ID(分布式链路追踪)
- 【推荐】日志分割
- 文件大小:单个日志文件最大 10MB
- 日志保留时间:7 天
- 日志格式:每日日志 logs/app-2025-03-03.1.log,超出 10MB 会自动切割
- 【推荐】Logback 配置
%d{yyyy-MM-dd HH:mm:ss.SSS} → 时间戳格式
%level → 日志级别
%thread → 线程名
%logger{36} → 限制类名长度
%method → 方法名
%line → 代码行号
%X{traceId} → MDC 变量(分布式追踪)
%msg%n → 日志消息
- 【推荐】日志存储策略
- Elasticsearch (ES):日志存储 & 查询引擎
- Logstash / Filebeat:日志收集 & 传输
- Kibana:日志分析 & 可视化展示
- Logback + JSON:日志格式化,支持 ELK 解析
7. 代码管理
7.1. 提交规范
- 【强制】提交信息格式
格式:type [标识]+‘:’+【业务模块】+具体页面/具体功能开发的描述,若是修复善治 bug,带上善治 bug id
类型(Type)
新增:【业务模块】新增feature,新功能开发
修复:【业务模块】修复bug,如修复禅道bug,带上禅道bug编码
文档:【业务模块】仅仅修改了注释/文档,如readme.md
样式:【业务模块】仅仅是对格式进行修改,如逗号、缩进、空格等。不改变代码逻辑。
重构:【业务模块】代码重构,没有新增功能或修复bug
移除:【业务模块】删除部分无用文件。
测试:【业务模块】测试用例,包括单元测试、集成测试。
更新:【业务模块】改变构建流程、或者增加依赖库、工具等。
回滚:【业务模块】版本回滚
| type | 含义 | 使用场景举例 |
|---|---|---|
| 新增 | ✨ 新功能 | 添加新页面、新接口、新配置 |
| 修复 | 🐛 修复问题 | 修复 Bug、逻辑错误、异常处理 |
| 重构 | 🔨 重构 | 优化已有代码结构但不改变功能 |
| 优化 | ⚡ 性能优化 | 提升查询效率、减少响应时间 |
| 样式 | 🎨 样式调整 | 格式化代码、统一命名、去除空格 |
| 文档 | 📝 文档变更 | 更新注释、接口文档、README 等 |
| 测试 | ✅ 测试相关 | 新增或修改单元测试、集成测试 |
| 更新 | ✅依赖更新 | 变更构建流程、增加依赖、更新工具 |
| 回滚 | ⏪ 回滚提交 | 回退之前的某次提交 |
| 合并 | 🔀 合并分支 | 合并 feature/dev/main 分支 |
- 【强制】git账号提人的名字需为中文,邮箱要用公司邮箱。
说明:Git 提交记录 需要清晰可读,避免混淆不同人员的提交记录,便于追踪责任人。
git config --global user.name "张三"
git config --global user.email "linchang@chinasie.com"
8. 数据库
8.1. 建表规范
- 【强制】表名规范格式:sie_ + 产品线 + _ + 功能
正例:
sie_mdm_customer //主数据产品线的客户表
- 【强制】iDME 模型命名规范(模型名)格式:Sie + 产品线 + 功能(大驼峰命名)
正例:
SieMdmCustomer
- 【强制】iDME 字段模型命名规范(模型字段)命名方式:大驼峰命名
正例:
CustomerName、ContactPhone
- 【强制】数据库字段命名规范(字段名)命名方式:全小写 + 下划线分隔
正例
customer_name、contact_phone
9. 跨服务调用
核心价值:防止级联故障,保障系统稳定性
9.1. 问题背景
在微服务架构中,服务间的依赖关系复杂,一个服务的故障可能引发连锁反应:
// ❌ 不检查直接调用的风险示例
public OrderDTO createOrder(OrderRequest request) {
// 直接调用库存服务
inventoryClient.deductStock(request.getSkuId(), request.getQuantity()); // 如果库存服务宕机,这里会抛出异常
// 直接调用用户服务
UserDTO user = userClient.getUser(request.getUserId()); // 如果用户服务不可用,订单创建失败
return orderRepository.save(request);
}
9.2. 使用规范
// ✅ 正确用法
boolean canAccessMpm = applicationBizHandle.isAvailableApplication(ServiceNameConstants.MPM_BIZ);
boolean canAccessUaa = applicationBizHandle.isAvailableApplication(ServiceNameConstants.UAA_BIZ);
// ❌ 错误用法
boolean result1 = applicationBizHandle.isAvailableApplication("mpm-biz"); // 禁止使用字符串
boolean result2 = applicationBizHandle.isAvailableApplication("MPM_BIZ"); // 禁止直接使用常量名
正例:
@Resource
private ApplicationBizHandle applicationBizHandle;
/**
* 安全调用MPM服务
*/
public <T> T safeInvokeMpm(Supplier<T> mpmInvoker, Supplier<T> fallback) {
if (applicationBizHandle.isAvailableApplication(ServiceNameConstants.MPM_BIZ)) {
return mpmInvoker.get();
}
log.warn("MPM服务不可用,使用降级方案");
return fallback.get();
}