# 代码审查规范 **Repository Path**: LuvSnow/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-06-24 - **Last Updated**: 2023-02-17 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 代码规范 ## 目的: > 规范化代码编写样式,方便开发人员在不同时间段迅速理解代码,快速进入开发阶段,减少熟悉代码的时间和理解业务需求。 > 减少代码逻辑的正确,去除大量耗费资源的操作,优化代码执行效率 ## 要求: > 开发人员严格按照规范要求编写代码,避免编写过多的个人特色的代码,减少其他开发人员理解代码的资源耗费 ## 代码规范 ### Java命名规范 #### 包命名 > **注意** 本规范以若依plus框架作为基础,使用非若依框架自行去除不合理的部分 > 1. 新项目如果以`zxyy-demo`作为基础开发,则采用新建module的形式,通过在`zxyy-admin`中引入包实现 > 2. 新项目新建的包必须为com.zxyy + 项目名 + 特定包名的形式出现,所有包名一律以英文单词全小写的形式,通过英文横杠(-)的形式拼接,原则上不超过3个单词,严格禁止使用拼音或者简拼的形式出现 > 3. 例如 基因库项目:zxyy-gene-data-bank/com.zxyy.bank.{domain|repository|mapper|...},通过在zxyy-admin中引入zxyy-gene-data-bank包的形式搭建项目 1. **domain** 数据库实体,与数据库完全相同,同时每个实体的属性需要JavaDoc备注 1. `domain.bo` 请求实体,例如数据库User,请求参数是以User为主要目标,如username,或者是用户的角色关联字段roleName,可以使用UserBo 2. `domain.vo` 返回实体,例如数据库User,返回参数是以User为主要目标,如用户的详细数据,或者用户数据并额外返回的roleVo数据,可以使用UserVO 3. `domain.{request|criteria}` 与数据库实体无关的请求参数,例如统计草场数据时的请求参数,PastureRequest/PastureQueryCriteria 4. 其他非数据库实体的,一律使用相关包分组,禁止存放在domain下,例如使用`domain.es`做包名,存放ES文档实体,如果有BO或VO,仿照 1 2 做分组处理 2. **repository包** Jpa数据层操作接口 3. **mapper包** MybatisPlus数据库操作 1. `mapper.generate` 存放由框架生成的默认mapper接口,一般命名为`xxxGenerateMapper.java` 2. `mapper` 存放非框架自带的,属于应用逻辑的mapper接口,通过继承 `mapper.generate` 里面的接口与数据库变动同步 3. `resources.mapper.项目名.generate` 存放由框架生成的默认mapper.xml,一般命名为`xxxGenerateMapper.xml` 4. `resources.mapper.项目名` 存放非框架自带的,属于应用逻辑的mapper.xml 4. **service包** 业务逻辑处理操作 1. `service` 存放业务接口 2. `service.impl` 存放业务接口的实现 5. **rest包** API调用的控制器,一般命名为`{rest|controller}` 6. **config包** 所有非框架自带的配置项,一律添加在这里,如果有需要再application.yml配置参数,同样存放在这里 7. **constants** 静态配置项,无需使用枚举的配置值,又不需要编写在application.yml中的 8. **enums包** 枚举值 #### 方法名 1. 所有方法一律以大写的驼峰形式的单词拼接,方法上必须使用JavaDoc的形式进行备注 ```java /** *

类名

* 类描述 *

类描述换行显示

* * @author 方法编写者 * @author 方法修改者 * @author ... **/ public class UserService extends BaseService { /** *

方法名

* 方法描述 *

方法描述换行显示

* * @param param 方法参数,至少是/ * @param otherParam / * @return 方法返回参数,至少是/ * @author 方法编写者 * @author 方法修改者 * @author ... */ public boolean doSomething(String param, String otherParam) { // 业务操作 } } ``` 2. 所有控制器接口,除上传文件,下载文件等特殊方法,严禁使用`(HttpRequest request)`的传参形式,通过逻辑获取调用`request.getParam()`等其他方法 来获取参数。必须使用`@RequestParam`、`@RequestBody`等注解的形式设定参数,并配置JavaDoc里对应的`@param`的中文备注 3. 所有控制器接口调用,除明确表示不记录的接口,都需要使用审计功能记录接口调用的参数 ```java /** * ... */ @RequiredArgsConstructor @RestController @RequestMapping("/bank/biologicalTissue") public class BiologicalTissueController { /** * ... */ @Log(title = "生物组织管理", businessType = BusinessType.INSERT) @RepeatSubmit() @PostMapping() public R add(@Validated(AddGroup.class) @RequestBody BiologicalTissueBo bo) { return toAjax(iBiologicalTissueService.insertByBo(bo) ? 1 : 0); } } ``` 4. 所有控制器接口,需要配置日志记录的,需完善`title`以及`businessType`字段,便与统一配置接口日志记录筛选 #### 属性名 1. 所有属性名一律使用小写开头的[**驼峰形式**](#实体类)单词拼接,并配备有JavaDoc备注,严禁使用拼音。 同时严格要求单词拼写,例如`araeId`,`wrokId`等 2. 类中`@Autowire`注解的类,必须使用`private`修饰,严禁使用其他修饰符修饰或不修饰,例如`protect UserService userService;` 或`UserService userService;` 3. 关于Spring自动注入,可以在类上启用`@RequiredArgsConstructor`注解,同时将相应需要注入的类使用`private final`修饰,即可减少注解`@AutoWire`或`@Resource` 4. application.yml中某些独特的配置以全小写单词加英文横杠(-)的形式拼接,同一类别的需归属在同一个上级下 ```yaml # ... # 第三方调用配置 third-part: # 主地址 host: "https://hzfy.bytespring.cn" # 获取消息 url: "/wx/getMessage" # 获取其他数据 other-url: "/pad/getData" # ... ``` 5. 对于yml中写好的配置如何调用,参考[示例](#配置获取) ### Js命名规范 #### 文件夹名 > 一般来说,大部分的前端框架文件夹都是以全小写的单个英文单词,如若特殊情况可以使用小写开头的驼峰形式英文单词组成文件夹名 > 同时也是严禁出现拼音或拼音缩写组成的文件夹名 #### 文件名 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 log.info("do something"); } } // 正确的操作 // find 方法可以修改为实际代码中的枚举操作 UserTypeEnum userTypeEnum = UserTypeEnum.find(user.getType()); if (ObjectUtil.isNotNull(userTypeEnum) && userTypeEnum.getCode().equals(UserTypeEnum.THREE.getCode())) { // do something log.info("do something"); } // 3为魔法值,不知道代表什么 // 必须通过枚举的形式调用 if (("3").equals(user.getType())) { log.info("do something"); } // 正确的形式 if (UerTypeEnum.THREE.getCode().equals(uesr.getType())) { log.info("do something"); } // 也可以配合MybatisPlus的@EnumValue做到直接使用Enum类的作用 if (UserTypeEnum.THREE.equals(user.getType())) { log.info("do something"); } } } ``` [**枚举样例**](#枚举) ### Java方法封装 1. 针对多次调用的方法,重复的代码,只是参数不同的方法,必须使用方法封装。 快速找到重复代码可以使用idea中的 `Alibaba Java Coding Guidelines` 插件快速找到。重复代码将自动提示。 2. 单个方法行数尽量不要超过80行,方法内行数超过30行则必须使用行内注释的形式,在操作的上一行进行行内注释。if判断条件可以在判断后的第一行注释 ```java public class UserServiceImpl implements UserService { /** * 从redis中获取消息数据 * * @return 包含有消息数据的返回体 **/ public ResponseEntity getMessageFromRedis() { // ... // 获取当前的登录用户数据 User user = Optional.ofNullable(getUser()) .orElseGet(User::new); // 或者使用抛错的形式 User user = Optional.ofNullable(getUser()) .orElseThrow(() -> { throw new ServiceException("用户不存在"); }); // 先校验数据,保证后续的操作都是在数据正确的情况下再操作 if (StrUtil.isBlank(user.getUserName())) { throw new ServiceException("用户不存在"); } // 确认数据是否正常 if (!DataPermissionHelper.getPermission(LoginHelper.getUser(), "param")) { throw new ServiceExcepiton("用户无权限"); } // 在确保数据都正确的情况下,再做其他的业务逻辑操作 // 判断是否可以获取数据 if ("abcd".equals(user.getUserType())) { // 某个业务操作 this.doSomething(user); // 某个业务操作 // 获取到的用户的消息队列信息 List messageDtoList = this.doNext(user); // 某些操作 return ResponseResult.success(messageDtoList); } else { // 否则走另外一个逻辑 log.info("do other thing"); return ResponseResult.success(messageDtoList); } } /** * 某个操作 * * @param user 用户数据 **/ private void doSomething(User user) { // ... // 设置用户的模式为第二种形式 user.setType(UserTypeEnum.THREE.getCode()); // .... } /** * 另一个操作 * * @param user 用户数据 * @return 消息队列 **/ 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); log.info("do something"); // ... return messageDtoList; } } ``` > **建议**:每一步都进行备注,方便后期修改业务逻辑时能迅速定位需要修改的位置,理解代码逻辑处理思路。 [**更多示例**](#业务逻辑实现) ### Js方法封装 > 前端上方法的封装要求与 [**后端**](#Java方法封装) 相同 ### 循环判断逻辑处理 > 无论是前后端,针对for循环以及if判断的嵌套,原则上禁止出现超过3次嵌套,同时严格禁止Java在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()) { // 找到了对应的用户 log.info("do something"); // 其他业务操作 } } // 其他业务操作 } } ``` > 对于数据库的操作或对于接口的调用,严格禁止在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); log.info("do something"); // ... 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错误拦截器配置 > 所有错误拦截器必须输出错误的信息,即在进行错误处理后,额外输出至日志中 #### Js错误拦截器配置 > 所有调用api的错误,必须打印至控制台 #### 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.zxyy.app.module; import lombok.Data; import xxxx; import javax.persistence.*; /** * 用户数据库实体 * * @author Emil.Zhang **/ @Data @EqualsAndHashCode(callSuper = true) @Table(name = "sys_user") public class User extends BaseEntity { /** * 唯一标识 */ @TableId("id") private Long userId; /** * 用户名 **/ private String userName; /** * 时间 **/ private Date timeDate; /** * 时间 * jdk8 以后,可以使用 * 但仅适用于mybatis-plus 3.4.2以上,否则将导致格式转换错误, * 或者使用java.util.Date */ private LocalDateTime localDateTime; /** * 逻辑删除字段,mybatis-plus需要配置 */ @TableLogic private Boolean delFlag; // ... } ``` ##### 枚举 ```java package com.zxyy.app.enums; import xxx; /** * 用户类型枚举 * * @author Emil.Zhang **/ @Getter @AllArgsConstructor public enum UserTypeEnum { /** * 一类型 **/ ONE("0", "一类型"), /** * 二类型 **/ TWO("1", "二类型"), /** * 三类型 **/ THREE("3", "三类型"); /** * 用于MybatisPlus框架自动处理枚举类映射到数据库 */ @EnumValue private final String code; /** * 用于渲染输出枚举到前端,显示的是该注解标记的值 */ @JsonValue private final String description; /** * 用于从前端接收Json字符串时,自动转换为对应枚举 * 当没有指定该注解时,将依据@JsonValue逆向解析 */ @JsonCreater 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.zxyy.app.config; 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.zxyy.app.service.impl; import xxxx; /** * 用户部分业务处理 * * @author Emil.Zhang */ @Slf4j @Service @RequiredArgsConstructor public class UserServiceImpl implements UserService { private final ThirdPartProperties thirdPartProperties; 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); // 将需要保存的数据添加至待保存中,最后统一保存 requireSaveList.addAll(userList); } for (User user : savedUserList) { // 对每个用户进行某些操作 this.toNext(user); } try { String url = "https://xxx.com/api/doSomething"; // 某个业务操作 HttpUtil.send(url, paramMap); } 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, UserType type) { if (UserType.ADMIN.equals(type)) { // 依据不同传参做操作 log.info("do something"); } else { 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()); // 某些操作 } } ``` ## 代码审查人员安排 ### 审查排班 1. 原则上,由一名主审人员和一名后端开发工程师以及一名前端开发工程师组成审核小组,依据规范进行代码审核。 2. 参与评审的人员将在审查前3天收到通知,请评审人员务必确保至少有半天的时间可以进行代码审查。 3. 确实无法参与评审的需提前与主审人员告知 ### 审查对象 原则上仅审查git版本控制管理的代码,并且只关注master分支,其余分支将不做任何要求 ### 审查结果 1. 审查结果将作为下月绩效考核的依据,自执行后第一次审查作为提示,第二次作为警告,第三次将进行扣除绩效的处理 2. 评定结果为严重级别的将在第二次出现时即进行扣除绩效的处理 ### 审查时间 审查为每月两次,一般为每月第一、三个星期三 ### 审查严重级别划分 1. 出现for循环内调用数据库或调用外部接口的,多次(2次或以上)出现视为严重错误 2. 出现命名不规范的,视为中度级别,要求立即整改,并在复审时提交处理结果 3. 出现备注不完善的,缺少对必要的方法,属性注释的,视为中度级别 4. 没有以标准JavaDoc的形式做备注的,视为低度级别 5. @param 等tag缺少中文注释,视为低度级别 ### 审查结果申诉 1. 对于审查结果有异议的,在2个工作日内交由主审人进行复审。 2. 对复审有异议的,提交至项目负责人处理。 3. 再次对复审出现异议的,不再予以受理。