代码规范落地实践

Categories:

背景

代码规范一直是个让人头疼的问题。尽管市面上已经存在了一些代码规范检查工具,如pmd和checkstyle,通用的规则已经涵盖了许多常见问题和技术规范。

如for循环的嵌套层数、可能出现的空指针等。但这些规则往往只是纯技术性的规则,无法满足不同团队的个性化业务需求。

因此,团队往往还需要针对自己的开发习惯和项目特点,制定更加具体化和个性化的规则,以便更好地约束和规范代码。

但是现实中,各团队大多依靠wiki等方式来约束和规范代码风格,这种只读的规范(《代码规范.txt》)往往难以执行落地。

规则的不断增加,团队内人员的流动,需要大量人力和物力进行代码评审,效率极低。

为此,我相信将代码规范内置在代码中,实现自动检查,将是更为有效的解决方案。

这样,开发人员无需再去阅读冗长的规范文字,管理人员也能够更加方便快捷地掌握整个代码的质量情况。

我们可以通过开发一款《代码规范.exe》来实现这一目标,在项目运行时进行代码规范自检查,若有不符合规范的地方,则及时给出提示,从而实现快速定位和修复问题的目标。

这样,无论是新人还是老手,都可以轻松地遵循代码规范,避免出现不必要的问题和错误。同时,也能够更好地提高代码质量,增强代码的可读性和可维护性。

总之,通过在代码中内置代码规范检查,我们可以更加有效地规范和约束代码风格,从而达到提高代码质量和效率的目的。

核心实现

PMD(Programming Mistake Detector) 是一个开源的静态代码检查工具,除了附带了许多通用规则外,用户还可以很方便地扩展自己的个性化规则。

PMD的核心原理是基于抽象语法树(Abstract Syntax Tree, AST)的遍历实现的。 其原理可以概括为以下几个步骤:

  1. PMD 使用 Java Parser 将代码解析成 AST,AST 以节点(Node)的形式表示代码结构。

  2. 遍历 AST。对于每个节点,调用该节点的 accept 方法。Node 类的 accept 方法会调用 Visitor 接口中对应类型的 visit 方法。

  3. 在 visit 方法中,我们可以检查节点的上下级节点,从而检测出代码中的某些模式或特殊问题。

因此,如果我们要扩展规则,只需要实现 Visitor 接口并在其 visit 方法中编写检查规则即可。

PMD更是为我们抽象了一个简单好用的AbstractJavaRule抽象类,让我们可以更简单方便地实现自定义规则。

public class MyCustomRule extends AbstractJavaRule {
    @Override
    public Object visit(ASTMethodDeclaration node, Object data) {
        // 检查方法名是否以do开头,则发出警告
        if (!node.getImage().startsWith("do")) {
            addViolation(data, node);
        }
        return super.visit(node, data);
    }
}

再来看一个抽象语法树的例子。比如我们想实现,if 语句后必须加 { 。

我们可以使用 PMD 提供的ast分析工具,来看看 if 后有 { 和没有 { 的 ast 分别是什么样子。

抽象语法树例子

如上图,可以看到如果 if 语句后有 {,那么 ast 结构里 IfStatement 后是先跟着一个 Statement ,再跟着一个 Block。

而如果 if 语句后没有 { ,那么 ast 结构里 IfStatement 下就没有 Block 了。

因此,我们要判断 if 后有没有 { ,只需要判断 IfStatement 后有没有 Block 即可。

代码检查的时机

被否的 git hooks

一开始想过用git hooks 的方式,在提交代码前执行检查,如果有违反规范的代码就不允许提交。像前端的husky就是如此。

但 git hooks 是本地钩子(存放在 .git/hooks ),如何实现 .git/hooks 的同步本身就是个问题(我期望可以全程自动化管理,而不是需要人工干预去做一些git hooks的设置)。

husky的原理是在npm install的时候生成git hook代码,因为每个前端仓库在下载代码后肯定都要执行 npm install 安装依赖。

而 npm 本身提供了 prepare 的 hook,只需要在 package.json 里配置一下即可让 husky 接管 git hooks 的管理。

// package.json
{
  "scripts": {
    "prepare": "husky install"
  }
}

而 java 和 Maven 却没有相关的 hook。即便有,也和前端处境不同。
前端开发人员肯定安装了 npm,npm肯定是一个可执行的命令。
而对于java开发人员来说,因为idea内置了一个 Maven ,因此本地不一定会安装 Maven 。
那就又涉及到环境变量 Path 的配置和 Maven 的安装(或找到idea内置的 Maven 安装位置)了。

依旧被否的 Maven 编译时

因为团队内全部成员都是使用intellij idea,开发人员的习惯是在intellij idea直接点run|debug来运行项目。

而idea的代码编译是用 Java Compile API,根本就没有使用 Maven ,无法实现拦截。

仍然被否的idea 插件 或 CI流水线

提供 idea 插件的话,依赖于开发人员自觉地进行一次主动扫描。

当然这个问题也可以通过 idea 的 File Watcher 插件来实现文件改动的时候自动触发扫描。

但是一来插件本身需要一定的开发成本,二来插件推广本身也有成本。
再次强调,我不希望每次来新成员的时候都需要新成员在自己电脑上设置一遍,甚至重新去教他一遍(写好一份配置指南让新成员照着做)。
我希望新成员将代码从仓库拉下来后在idea打开即可开始编码。

我觉得最好的方式的就是代码内置质量检查。

CI流水线的话就是一个反馈滞后的问题。你得把代码push上去触发构建了,才会知道代码有问题。

同时还会有本地规则和CI上的规则保持同步的问题。

最终采用的程序运行时

项目内 pom.xml 里引入一个依赖。因为项目本身是 SpringBoot,所以利用其 spring.factories 机制实现程序启动时自动加载代码扫描程序。

以编程式的方式启动PMD,检查src/main/java目录下的Java源代码(在服务器端直接跳过代码扫描)。

如果存在严重级别的代码问题,直接中断 SpringBoot 的运行。

如果你把不符合规范的代码提交了上去,还暴露了你根本就没有进行本地自测。

因为你改了代码,好歹得本地测试一下吧,那你启动项目就必然触发代码检查。

示例规则

Service层不允许获取当前登录用户

获取当前登录用户应该是Controller层干的事,不应该在Service层获取当前登录用户。

在Controller层获取到当前登录用户后,将用户信息以参数形式传给Service

Feign接口必须返回R<>结构

public interface UserApi {
    
    User getById(String id);

    R<User> getById(String id);
}

其实一个项目里全部统一用R或全部统一不用R,都可以。
但是,不能混着用。一些用R,一些不用R,是会出问题的。

如果想全部不用R,那么当服务端出错时,应该返回非 2XX 的 Http Status Code。
这样 FeignClient 在收到时就会知道请求出错了。

但如果想全部统一使用R,那在异常抛出时一般也会在全局异常处理里统一返回 2XX 的 Http Status Code和R<>结构的错误报文,
然后约定好客户端那边统一对R结构进行判断。

在这样的约定下,如果你不用R包一层,直接返回User,一旦你的方法内出错后被全局异常处理拦截后实际返回的是R<>错误报文,
但因为是Http Status Code = 200,所以 FeignClient 会将你的返回报文尝试转为 User,这时你的方法内抛出的原始错误就被吃掉了, 变成了JSON parse error。

写在最后

理解规范比执行规范更重要

架构师的水平往往决定了一个系统的设计上限(到底能有多好),
但是系统的下限(到底能有多烂)却是由一线开发的程序员决定的。
就算架构师设计的架构再好,如果架构、规范得不到落地,系统最终也会变成一坨屎。

但是,执行规范的前提是大家能理解规范
为什么会制定一个这样的规范?它解决了什么问题?带来了什么好处?如果不这样做会有什么问题或麻烦?

如果这些问题不回答好,一些初级成员可能并不理解这些规范,甚至不一定认可这些规范。
不理解或理解错了的话,可能就会写出一些似是而非的代码来。即表面看起来符合规范,实际却和规范的本意背道而驰。
不认可的话,可能就会偷偷按自己想法来,就赌你代码review时漏了没发现。
就如示例规则里的方法返回类型要不要用R结构包装一层一样,他可能是不小心漏了,也可能是仅仅觉得这是一个无关紧要的编码风格。

给开发人员说清楚规范的背景、目的、作用是件一举多得的事。
在讨论规范的过程中,互相印证,你这样设计是考虑了哪些场景?如果是我设计我会怎样处理?
大家的差异在哪?是你思考漏了还是我漏了?
在各种观点的碰撞中,大家充分交流,我相信大家的能力都会能得到大幅度的提高。

如果你觉得本文对你有帮助或不错,可略表心意,请我喝一杯冰可乐。

Comments