# 代码审查规范 **Repository Path**: ilooktech/code-review-specification ## Basic Information - **Project Name**: 代码审查规范 - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 1 - **Created**: 2021-08-11 - **Last Updated**: 2021-08-11 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 代码审核规范(试行) ## 目的: 规范化代码编写样式,方便开发人员在不同时间段迅速理解代码,快速进入开发阶段,减少熟悉代码的时间和理解业务需求。 减少代码逻辑的正确,去除大量耗费资源的操作,优化代码执行效率 ## 要求: 开发人员严格按照规范要求编写代码,避免编写过多的个人特色的代码,减少其他开发人员理解代码的资源耗费 ## 代码规范 ### 命名规范(后端) #### 包命名 包必须为com.wmjsoft + 特定包名的形式出现,所有包名一律以英文单词全小写的形式,通过英文横杠(-)的形式拼接,原则上不超过3个单词,严格禁止使用拼音或者简拼的形式出现 1. **domain包** 数据库实体,与数据库完全相同,同时每个实体的属性需要JavaDoc备注 2. **repository包** Jpa数据层操作接口,myBatis的DaoImpl实现等 3. **service包** 逻辑处理操作,包含有dto(输出实体)包,vto(输入实体)包,impl(接口实现)包,接口直接放置在service包下 4. **rest包** API调用的控制器 5. **config包** 配置项 5. **enums包** 枚举值 #### 方法名 1. 所有方法一律以大写的驼峰形式的单词拼接,方法上必须使用JavaDoc的形式进行备注 ```java /** * 类注释,描述类的用途 * * @author Emil.Zhang **/ public class UserService extends BaseService { /** * 方法注释,描述方法的用途,方法名使用小写开头的驼峰标识 * * @param param 方法的传参,描述参数具体是什么要求 * @return 方法执行后的结果,描述方法执行后的效果 * @author Emil.Zhang **/ public boolean doSomething(String param) { } } ``` 2. 所有api接口,除上传文件,下载文件等特殊方法,严禁使用`(HttpRequest request)`的形式,通过逻辑获取调用` request.getParam() `等其他方法 来获取参数。必须使用` @RequestParam `等注解的形式设定参数,并配置JavaDoc里对应的` @param `的中文备注 3. 所有api接口调用,除明确表示不记录的接口,都需要使用审计功能记录接口调用的参数。具体如何调用审计记录数据,需询问相应项目的负责人或架构师 #### 属性名 1. 所有属性名一律使用小写开头的[**驼峰形式**](#实体类)单词拼接,并配备有JavaDoc备注,严禁使用拼音。 同时严格要求单词拼写错误,例如grouupId,wrokId等明显的单词拼写错误 2. 类中`@Autowire`注解的类,必须使用`private`修饰,严禁使用其他修饰符修饰或不修饰,例如`protect UserService userService;` 或`UserService userService;` 3. application.yml中某些独特的配置以全小写单词加英文横杠(-)的形式拼接,同一类别的需归属在同一个上级下 ```yaml ... # 第三方调用配置 third-part: # 主地址 host: "https://hzfy.bytespring.cn" # 获取消息 url: "/wx/getMessage" # 获取其他数据 other-url: "/pad/getData" ... ``` 4. 对于yml中写好的配置如何调用,参考[示例](#配置获取) ### 命名规范(前端) #### 文件夹名 一般来说,大部分的前端框架文件夹都是以全小写的单个英文单词,如若特殊情况可以使用小写开头的驼峰形式英文单词组成文件夹名 同时也是严禁出现拼音或拼音缩写组成的文件夹名 #### 文件名 1. 文件名尽量以大写字母开头的驼峰形式进行命名,禁止出现拼音或拼音简拼组成的文件名 2. 文件命名原则上禁止出现任何特殊字符以及数字,例如Map1.vue,至少是提交代码到master分支时禁止出现 #### 方法名 前端代码中每个调用方法必须使用明确的动作加对象的形式进行方法命名 1. 明确的操作例如 `add`, `delete`, `update`, `get`等 2. 后面加上类名 `User`, `Map`, `Book`, `Message`等 3. 如果获取到的预期是集合或键值对的形式,要在类名后加上明确的`List`或`Map` 4. 最后可以加上 By条件,例如 `ByGroupId` ```javascript /** * 依据唯一标识获取用户数据集 * * @param groupId 用户分组唯一标识 * @return {array} 用户数据集 * @author Emil.Zhang */ function getUserListByGroupId(groupId) { // 业务逻辑 return userList; } /** * 更新用户数据 * * @param user 更新后的用户数据 * @return {boolean} 更新是否成功 * @author Emil.Zhang */ function updateUser(user) { // 业务逻辑 return true; } /** * 创建用户数据 * * @param user 创建的用户数据 * @return {boolean} 是否创建成功 * @author Emil.Zhang */ function addUser(user) { //业务逻辑 return true; } /** * 依据用户唯一标识删除用户 * * @param id 用户唯一标识 * @return {boolean} 是否删除成功 * @author Emil.Zhang */ function deleteUserById(id) { // 业务逻辑 return true; } ``` #### 属性名 所有属性名一律使用小写开头的[**驼峰形式**](#实体类)单词拼接,并配备有双斜杠的行内注释,严禁使用拼音。 同时严格要求单词拼写错误,例如grouupId,wrokId等明显的单词拼写错误 ```javascript function doSomething() { // ... // 获取用户数据集 let userList = this.getUserList() // 获取高德地图的Map键值对数据 const aMapMap = this.getLocation() // 获取当前的位置数据 var location = this.getNowLocationInfo() // ... } ``` ### 枚举的应用 1. 针对方法中的一些参数,严禁出现魔法值 2. 针对循环匹配值的形式,必须使用枚举中的方法来找数据,而不是循环判断 ```java /** * 用户业务操作 * * @author Emil.Zhang **/ public class UserService { /** * 某项操作 * @author Emil.Zhang **/ public void doSomeThing() { // 错误的操作 for (User user : userList) { if (UserTypeEnum.THREE.getCode().equals(user.getType())) { // do something } } // 正确的操作 // find 方法可以参考实例中的枚举操作 UserTypeEnum userTypeEnum = UserTypeEnum.find(user.getType()); if (ObjectUtil.isNotNull(userTypeEnum) && userTypeEnum.getCode().equals(UserTypeEnum.THREE.getCode())) { // do something } // 3为魔法值,不知道代表什么 // 必须通过枚举的形式 if (("3").equals(user.getType())) { } // 正确的形式 if (UerTypeEnum.THREE.getCode().equals(uesr.getType())) { } } } ``` [**枚举样例**](#枚举) ### 方法封装(后端) 1. 针对多次调用的方法,重复的代码,只是参数不同的方法,必须使用方法封装。 快速找到重复代码可以使用idea中的 `Alibaba Java Coding Guidelines` 插件快速找到。重复代码将自动提示。 2. 单个方法行数不允许超过80行,方法内行数超过30行则必须使用行内注释的形式,在操作的上一行进行行内注释。if判断条件可以在判断后的第一行注释 ```java public class UserService extends BaseService { /** * 从redis中获取消息数据 * * @return 包含有消息数据的返回体 * @author Emil.Zhang **/ public ResponseEntity getMessageFromRedis() { // ... // 获取当前的登录用户数据 User user = getUser(); // 判断是否可以获取数据 if (user != null && !"".equals(user.getUsername())) { // 某个业务操作 this.doSomething(user); // 某个业务操作 // 获取到的用户的消息队列信息 List messageDtoList = this.doNext(user); // 某些操作 return ResponseResult.success(messageDtoList); } else { // 如果不满走某个条件 return ResponseResult.error("缺少关键数据"); } } /** * 某个操作 * @param user 用户数据 * @author Emil.Zhang **/ private void doSomething(User user) { // ... // 设置用户的模式为第二种形式 user.setType(UserTypeEnum.THREE.getCode()); // .... } /** * 另一个操作 * * @param user 用户数据 * @return 消息队列 * @author Emil.Zhang **/ private List doNext(User user) { // ... List messageDtoList = MyDao.find(Message.class) .selectFiled("id", "message", "dateTime") .left(User.class) .selectFiled("name as userName") .on("userId", "=", "id", Message.class) .where() .eq("deleteStatus", 0) .eq("userName", user.getUserName()) .list(MessageDto.class); // ... return messageDtoList; } } ``` **建议**:每一步都进行备注,方便后期代码回顾的时候能迅速理解当时逻辑思路。 [**更多示例**](#业务逻辑实现) #### 方法封装(前端) 前端上方法的封装要求与 [**后端**](#方法封装(后端)) 相同 ### 循环判断逻辑处理 无论是前后端,针对for循环以及if判断的嵌套,原则上禁止出现超过3次嵌套,同时严格禁止在for内调用数据库,或者其他第三方接口。 需要获取数据的,一律使用一次性读取全部数据list,通过 `stream().collect(Collectors.groupingBy())` 的形式分组,在for循环中调用map的形式获取数据。 同时在for循环的第一行使用双斜杠的形式备注该循环的含义,在if操作的第一行备注该判断的条件含义。 ```java public class UserService extends BaseService { // ... public void doSomething() { // 其他业务操作 for (Group group : groupList) { // 循环每个分组,依据分组找到用户,随后进行某些操作 if (group.getId() == user.getGroupId()) { // 找到了对应的用户 // 其他业务操作 } } // 其他业务操作 } } ``` 对于数据库的操作或对于接口的调用,严格禁止在for循环内调用,可以通过提前获取全体数据,再依据某个条件按筛选的形式处理 ```java public class UserService extends BaseService { // ... public void doSomething() { // 正确的用法 // 其他业务逻辑 // 找到的用户数据 List savedUserList = userRepository.findByName(name); // 依据groupId分组的已保存的用户数据 Map> savedUserGroupBySomePropMap = savedUserList.stream().collect(Collectors.groupingBy(User::groupId)); for (Group group : groupList) { // ... List userList = savedUserGroupBySomePropMap.get(group.getId()); // ... } // 严格禁止的用法 // 其他业务逻辑 for (Group group : groupList) { // 循环的操作为了什么,之后会进行什么操作 // 其他业务逻辑 // 循环调用数据库,会导致严重的执行时间过久问题 List userDtoList = this.findByNameAndGroupId(name, group.getId()); } } /** * ... **/ private List findByNameAndGroupId(String name, Long groupId) { // ... List userDtoList = MyDao.find(User.class) .selectFiled("name as userName", "databaseName as otherOutPutName") .where() .like("name", name) .eq("groupId", groupId) .list(UserDto.class); // ... return userDtoList; } } ``` [**更多示例**](#业务逻辑实现) **建议**:备注应尽可能详细,避免后期回顾代码时还需要找到属性的备注来理解逻辑思路 ### 数据库操作 1. 所有需要调用数据库的操作,必须使用`@Transactional`注解到对应的方法上 2. 严禁在操作数据库时抓取错误后不抛出,导致事务的注解失效 ### 日志 #### logback.xml 1. 所有的日志输出一律使用slf4j的形式进行配置,同时 **必须** 使用 `logback-(dev, prod, test).xml` 2. 配置日志的输出格式为 `%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n` 即: `日期 时间 线程 级别 输出操作的代码所在文件.java – 日志信息` **例如** `2021-06-24 13:32:65.650 main INFO HelloWorld.java – 这是日志的输出的信息` 3. 除开发环境外,其余环境严禁使用控制台输出日志,必须保存相应的日志级别至对应的文件夹下的info,error文件夹内 #### 错误拦截器配置(Java) 所有错误拦截器必须输出错误的信息,java端统一使用 ```java log.error("服务器错误{}",exception.getMessage(),exception); ``` #### 错误拦截器配置(Javascript) 所有调用api的错误,必须打印至控制台,使用 ```javascript console.error() ``` #### 日志输出(Java) 1. 所有可能的try catch 错误抓取,以及调试过程,严禁使用System.out.printIn()的形式,必须使用log.error或其他等级的输出 2. 所有的同步、定时任务,必须在方法前后标记方法开始或结束,并在最后输出方法的执行时间 ### 部署 1. Windows下严禁使用java -jar的形式直接调用,导致cmd窗口常驻。 尝试学会使用 `RunHiddenConsole.exe`,或者将java包设置为服务的形式运行 2. Linux下必须使用后台运行的方式,通过 `nohup xxx /deploy/app/log/console.log 1>&2 &` 命令,部署在后台,或使用docker部署镜像,并需要将日志文件映射至主机,便于查找日志文件 ### 样例 ##### 实体类 ```java package com.wmjsoft.modle; import lombok.Data; import xxxx; import javax.persistence.*; /** * 用户数据库实体 * * @author Emil.Zhang **/ @Data @AllArgsConstructor @NoArgsConstructor @Builder @Table(name = "biz_h5_menu_detail") public class User extends BaseEntity { /** * 用户名 **/ @Column("userName") @Comment("用户姓名") @ColDefine(notNull = true) private String userName; /** * 名称 **/ @Column("name") @Comment("某些特定名称") @ColDefine(notNull = true) private String name; // ... } ``` ##### 枚举 ```java package com.wmjsoft.enums; import xxx; /** * 用户类型枚举 * * @author Emil.Zhang **/ @Getter @AllArgsConstructor public enum UserTypeEnum { /** * 一类型 **/ ONE("0", "一类型"), /** * 二类型 **/ TWO("1", "二类型"), /** * 三类型 **/ THREE("3", "三类型"); private final String code; private final String description; public static UserTypeEnum find(String code) { for (UserTypeEnum value : UserTypeEnum.values()) { if (code.equals(value.getCode())) { return value; } } return null; } } ``` ##### 配置获取 ```yaml .... third-part: host: http://localhost:8080 url: /helloWorld/ .... ``` ```java package com.wmjsoft.properties; import xxx; /** * yml中配置的关于第三方请求的配置信息 * * @author Emil.Zhang **/ @Data @Component // 设置yml中的third-part下的所有配置信息,自动填充到对应的属性名中 @ConfigurationProperties("third-part") public class ThirdPartProperties { /** * 请求地址 **/ private String host; /** * api地址 **/ private String url; } ``` ##### 业务逻辑实现 ```java package com.wmjsoft.service.impl; import xxxx; /** * 用户部分业务处理 * * @author Emil.Zhang */ @Slf4j @Service public class UserService implements BaseService { @Autowired private final ThirdPartProperties thirdPartProperties; @Autowired private final SchoolService schoolService; // ... /** * 依据名称找到用户数据,并将其状态设置为起始状态 * * @param name 用户组名 * @return 找到的用户数据集合 * @author Emil.Zhang */ @Override @Transactional(rollbackFor = Exception.class) public List updateTypeForUsers(String name) { // 实际的业务操作,超过了80行,强制要求每一步都需要有注释 String param = ""; // 其他业务操作 // 提交第三方接口调用 String postResult = HttpUtil.post(thirdPartProperties.getHost() + thirdPartProperties.getUrl(), param); // 解析返回的数据 JSONObject postJsonObject = JSONUtil.parse(postResult); // 其他业务操作 // 拼接名称 if (StrUtil.isNotEmpty(name)) { // 如果名称不为空 name = StrUtil.format("%{}%", name); } // 属性名建议以saved和requireSaving加后缀的形式区分已存和待存 // 如果一行代码字数过长(一般指观看不方便时),建议在等号后换行,便于查看 // 获取用户数据 List savedUserList = this.findByName(name); // 依据分组Id将用户分组 Map> savedUserGroupByGroupIdMap = savedUserList.stream().collect(Collectors.groupingBy(User::getGroupId)); // 依据某一个数值找到的学校数据 List savedSchoolList = schoolService.findByType(SchoolTypeEnum.SOMETHING.getCode()); for (School school : savedSchoolList) { // 循环每一个学校,进行某些业务操作 // 错误的调用,在for循环内调用数据库会导致数据库压力过大, // 调用时间过长,甚至造成服务器宕机 // List userList = // userRepository.findByNameAndGroupId(name, groupItem.getId()); // 正确的操作,通过循环外一次性查询可能的数据,通过不同分组来获取需要的部分 List userList = savedUserGroupByGroupIdMap.get(groupItem.getId()); for (User user : userList) { // 某些业务操作 this.doSomething(user, savedSchoolList, UserTypeEnum.THREE.getCode()); } // 错误的操作,严禁在for循环内对数据库操作 // userRepository.saveAll(userList); } for (User user : savedUserList) { // 对每个用户进行某些操作 this.toNext(user); } try { // 某个业务操作 } catch (Exception exception) { log.error("某个操作出错了"); // 所有抓取到的错误,在输出错误日志之后必须抛出 throw new Exception("某个错误发生了"); } // 正确的操作,在最外部操作了数据库 this.saveAll(userList); schoolService.saveAll(savedSchoolList); } /** * 封装出来的方法 * 依据选择的类型设置给用户,同时更新学校数据 * * @param user 用户实体 * @param savedSchoolList 数据库中的学校数据 * @param type 封装出来的独特的参数 * @author Emil.Zhang */ private void doSomething(User user, List savedSchoolList, String type) { // 封装出来重复调用的业务逻辑 user.setType(type); for (School school : savedSchoolList) { // 业务逻辑 // 设置学校的状态为放假中 school.setStatus(SchoolStatusEnum.HOLLYDAY.getCode()); // 业务逻辑 } // 这里是一个陷阱,原先富阳二期的月考核定时任务就是这么操作的 // 虽然方法提取出来了,但是这个方法实际上是在循环内调用的 // 因此需要将学校数据当参数传进来,操作后在外部进行保存操作 // schoolService.saveAll(schoolList); } /** * ... **/ private void toNext(User user) { // 某些操作 user.setStatus(UserStatusEnum.GOOD.getCode()); // 某些操作 } } ``` #### API调用 ```java ``` ## 审查人员 ### 审查排班 1. 原则上,由一名主审人员和一名后端开发工程师以及一名前端开发工程师组成审核小组,依据规范进行代码审核。 2. 参与评审的人员将在审查前3天收到通知,请评审人员务必确保至少有半天的时间可以进行代码审查。 3. 确需离开公司,无法参与评审的需提前与主审人员告知 ### 审查对象 原则上仅审查通过git版本控制管理的代码,并且只关注master分支,其余分支将不做任何要求 ### 审查结果 1. 审查结果将作为下月绩效考核的依据,自执行后第一次审查作为提示,第二次作为警告,第三次将进行扣除绩效的处理 2. 评定结果为严重级别的将在第二次出现时即进行扣除绩效的处理 ### 审查时间 审查为每月两次,一般为每月第一个星期三,每月第三个星期三 ### 审查严重级别划分 1. 出现for循环内调用数据库或调用外部接口的,多次(2次或以上)出现视为严重错误 2. 出现命名不规范的,视为中度级别,要求立即整改,并在复审时提交处理结果 3. 出现备注不完善的,缺少对必要的方法,属性注释的,视为中度级别 4. 没有以标准JavaDoc的形式做备注的,视为低度级别 5. @param 等tag缺少中文注释,视为低度级别 ### 审查结果申诉 1. 对于审查结果有异议的,在2个工作日内交由主审人进行复审。 2. 对复审有异议的,提交至项目负责人处理。 3. 再次对复审出现异议的,不再予以受理。