擴展阿里p3c實現自定義程式碼規範檢查

  • 2019 年 10 月 25 日
  • 筆記

 前段時間fastjson報出了漏洞,只要打開setAutoType特性就會存在風險,自己測試環境的一個項目被揪出來了-_-!。雖然改動很小,但就是覺得憋屈。fastjson還是挺好的,想著禁用的話太可惜,用的話又要注意安全,就想著找款工具提示下在用fastjson的時候不要打開這個特性。剛好阿里開源了p3c(https://github.com/alibaba/p3c),一款程式碼規範的檢查工具,有對應的ide插件,能在編碼過程中對設置的規則進行提示,便打算對它進行拓展,增加對fastjson檢查是否打開setAutoType特性的檢查。

 p3c主要包括3部分:

  • PMD實現(p3c-pmd):使用PMDhttps://pmd.github.io/來實現程式碼規範檢查
  • Intellij IDEA插件
  • Eclipse插件

 《阿里巴巴Java開發手冊》中的大部分規則都是在p3c-pmd模組中實現的,該部分也是這節研究的主要部分。

1. PMD

 p3c使用了PMD。PMD是一款靜態程式碼掃描工具,該工具可以做到檢查Java程式碼中是否含有未使用的變數、是否含有空的抓取塊、是否含有不必要的對象等。PMD使用JavaCC生成解析器來解析源程式碼並生成AST(抽象語法樹),通過對AST的檢查可以直接從源程式碼文本層面來對程式碼進行檢查,在PMD內部稱為規則。即是否符合規則指的是,窮舉源碼各種可能的寫法,然後在AST上檢查是否出現。而規則的實現,重點便在對AST的處理上。

1.1. AST

 關於AST的介紹網上有很多,可以直接搜索,這裡重要提兩點:

  • AST是源程式碼的抽象語法結構的樹狀表示
  • 抽象語法樹並不依賴於原語言的語法,也就是說同語法分析階段所採用的上下文無關

 PMD使用JavaCC來生成AST。關於JavaCC也可以在網上查看相關資料,這裡不多介紹,只要知道JavaCC是一個詞法分析生成器和語法分析生成器便行。

1.2. 自定義規則

 PMD官方文檔介紹了自定義規則的實現步驟,過程比較清晰,這裡不贅述,只介紹下本文需要設計的步驟。

1.2.1. 定義規則集

 PMD的規則需要配置在XML文件中

  • 新建如下的空文件表示規則集
    <?xml version="1.0"?>          <ruleset name="Custom Rules"          xmlns="http://pmd.sourceforge.net/ruleset/2.0.0"          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"          xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 https://pmd.sourceforge.io/ruleset_2_0_0.xsd">            <description>              My custom rules          </description>              <!-- Your rules will come here -->        </ruleset>      ```    * 在上面的``` ruleset ``` 標籤中引用自定義規則        ```      <rule ref="pathToYourOwnClass" />      ```    * 配置規則集    ###### 1.2.2. 配置規則    &emsp;可以在 ``` rule ``` 標籤中對某一規則進行配置    * 配置提示消息和告警級別        ```      <rule ref="pathToYourOwnClass"        message="message to show" >        <priority>5</priority>      </rule>      ```      告警優先順序分為1-5個level,1最高,5最低    * 配置運行參數        可以通過 ``` properties ``` 和 ``` propertie ```對類屬性賦值    ###### 1.2.3. 編寫規則    &emsp;規則的編寫比較簡單,PMD已經給我們做好了配套的開發框架和工具,只要確定後規則出現的情況,按照固定的模式去編寫即可。    * 確定實現方式        可以使用純Java方式實現,也可以使用XPath方式實現。        對於純Java方式,PMD框架本身實現了對AST數的遍歷,用戶只要在遍歷各個節點的時候,對自定義規則的各種情況進行分析判斷即可,過程類似與DOM文件的SAX解析,以流和事件的方式來解析AST內容。        對於XPath方式,則是將AST作為一個XML數,以XPath的方式來遍歷解析內容。    * 根據測試程式碼確定可能出現的情況        PMD自帶了一個designer,可以用來生成對應程式碼的AST內容。對於本文需要達到的效果,涉及到如下程式碼:        ```        import com.alibaba.fastjson.parser.ParserConfig;        public class NegativeExample {            public static void main(String[] args) {              ParserConfig.getGlobalInstance().setAutoTypeSupport(true);          }      }        ```        在designer中可以得到如下的AST內容      ![file](https://img2018.cnblogs.com/blog/1812801/201910/1812801-20191025160959878-590799600.jpg)        具體內容這裡就不貼出來了,可以下載desigin然後貼上上面的程式碼就會有。        對於本文的需求,需要確定在有import ``` ParserConfig ``` 類的時候,調用了 ``` setAutoTypeSupport ``` 方法且參數為 ``` true ```。當然這個條件是不夠嚴謹的,還需要判斷是否調用了 ``` ParserConfig ```,也要考慮有通過import導入直接寫全限定名的。但考慮到一般的寫法,以及ide的格式化處理,這樣處理已經可以滿足大部分場景了。    * 編寫規則實現類        確定了規則的判斷條件,就可以著手寫程式碼了。        對於Java方式,可以直接繼承 ```net.sourceforge.pmd.lang.java.rule.AbstractJavaRule ``` 類,然後重寫各種節點的visit方法即可。        例如如下,用於判斷是否有import ``` ParserConfig ``` 類        ```      private final String PARSER_CONFIG_IMPORT_NAME = "com.alibaba.fastjson.parser.ParserConfig";        private boolean hasImport = false;        @Override      public Object visit(ASTImportDeclaration node, Object data) {          if (PARSER_CONFIG_IMPORT_NAME.equals(node.getImportedName())) {              hasImport = true;          }          return super.visit(node, data);      }      ```        對於XPath方式,可以通過繼承 ``` net.sourceforge.pmd.lang.rule.XPathRule ``` 類,重寫 ``` setXPath ``` 方法設定對應的XPath即可。上面提到過,可以通過 ``` property ``` 配置對類的屬性進行賦值,因而,對於XPat方式,可以在xml配置中進行如下設置來啟用XPath。        ```      <rule name="My Rule"            language="java"            message="violation message"            class="net.sourceforge.pmd.lang.rule.XPathRule">          <description>  Rule Description           </description>           <priority>3</priority>           <properties>               <property name="xpath">                   <value>                   <![CDATA[  --- here comes your XPath expression  ]]>                   </value>               </property>           </properties>      ```    ###### 1.2.4. 測試規則    &emsp;PMD推薦對於每個規則,至少要有一個正向和逆向的測試用例,來驗證規則出現和不出現的情況。對於規則的測試,PMD也提供了一套框架,只要按照約定好的方式添加xml測試文件即可。    &emsp;PMD約定了幾個規則,用來載入測試案例    * 測試類要繼承 ``` net.sourceforge.pmd.testframework.PmdRuleTst ```類,該整合了Junt,可以在裡面增加需要的測試方法。    * 對於在 ``` src/test/resource ``` 和測試類對應的路徑下增加一個xml目錄,在增加同第一步同名的xml文件,該文件用於書寫測試集。        例如官網給出的例子        規則實現類路徑如下:        ```  net.sourceforge.pmd.lang.java.rule.bestpractices.AbstractClassWithoutAbstractMethodTest        ```        測試案例集如下:        ```      src/test/resources/net/sourceforge/pmd/lang/java/rule/bestpractices/xml/AbstractClassWithoutAbstractMethod.xml        ```        xml的規則則可以參考官網介紹,這裡不再贅述。    #### 2. p3c-pmd    &emsp;程式碼規範實現的主要模組,使用pmd來實現。p3c-pmd模組在程式碼組織上很工整,可以按照相同的模式增加自定義的規則/規則集。    &emsp;對於本文需求,打算在該模組的基礎上增加一個extend模組,用於實現自定義規則集。如下,為對應的源碼路徑好規則集路徑    ![file](https://img2018.cnblogs.com/blog/1812801/201910/1812801-20191025161000108-1340099359.jpg)    ![file](https://img2018.cnblogs.com/blog/1812801/201910/1812801-20191025161000368-199680801.jpg)    &emsp;本文採用純Java方式實現規則,按照1.2.3.節的描述,可以得到如下的實現類  

public class AvoidFastJsonAutoTypeSupportRule extends AbstractAliRule {

private final String PARSER_CONFIG_IMPORT_NAME = "com.alibaba.fastjson.parser.ParserConfig";    private final String SET_AUTO_TYPE_SUPPORT_NAME = "setAutoTypeSupport";    private boolean hasImport = false;      @Override  public Object visit(ASTImportDeclaration node, Object data) {      if (PARSER_CONFIG_IMPORT_NAME.equals(node.getImportedName())) {          hasImport = true;      }      return super.visit(node, data);  }    @Override  public Object visit(ASTPrimaryExpression node, Object data) {      if (hasImport) {          int size = node.jjtGetNumChildren();          for (int i = 0; i < size; i++) {              Node child = node.jjtGetChild(i);              String imageName = null;              if (child instanceof ASTPrimaryPrefix) {                  ASTPrimaryPrefix prefix = (ASTPrimaryPrefix) child;                  imageName = prefix.jjtGetChild(0).getImage();              }else if (child instanceof ASTPrimarySuffix){                  ASTPrimarySuffix suffix = (ASTPrimarySuffix) child;                  imageName = suffix.getImage();              }                if (imageName == null) {                  continue;              }else if (imageName.endsWith(SET_AUTO_TYPE_SUPPORT_NAME)){                  ASTPrimarySuffix argumentSuffix = (ASTPrimarySuffix) node.jjtGetChild(i + 1);                  try {                      List<Node> booleanArgs = argumentSuffix.findChildNodesWithXPath("//PrimaryPrefix/Literal/BooleanLiteral");                      if (booleanArgs.size() == 1) {                          ASTBooleanLiteral booleanLiteral = (ASTBooleanLiteral) booleanArgs.get(0);                          if (booleanLiteral.isTrue()) {                              ViolationUtils.addViolationWithPrecisePosition(this, argumentSuffix, data,                                  I18nResources.getMessage("java.extend.AvoidFastJsonAutoTypeSupportRule.rule.msg" ));                          }                      }                  } catch (JaxenException e) {                      e.printStackTrace();                  } finally {                      break;                  }              }          }      }      return super.visit(node, data);  }

}

  &emsp;對應規則集中的配置為  


java.extend.AvoidFastJsonAutoTypeSupportRule.rule.desc 1
<![CDATA[
Negative example:
import com.alibaba.fastjson.parser.ParserConfig;

  public class NegativeExample {          public static void main(String[] args) {            ParserConfig.getGlobalInstance().setAutoTypeSupport(true);        }    }

]]>

  &emsp;這裡有幾點需要注意的,類 ``` AvoidFastJsonAutoTypeSupportRule ``` 繼承自 ``` com.alibaba.p3c.pmd.lang.java.rule.AbstractAliRule ```,AbstractAliRule繼承自AbstractJavaRule,重寫了setDescription,setMessage和addViolationWithMessage等方法,這裡提到的3個方法,增加了多語言支援。p3c-pmd使用Resource Bundle來提供多語言支援。每個消息都有一個唯一id來對應,p3c-pmd通過重寫方法,將方法參數映射為消息的id,以統一消息的配置。如下為本地對應的消息提示內容  
<!--extend-->  <entry key="java.extend.AvoidFastJsonAutoTypeSupportRule.rule.msg">      <![CDATA[不要打開fastjson的setAutoTypeSupport特性]]>  </entry>  <entry key="java.extend.AvoidFastJsonAutoTypeSupportRule.rule.desc">      <![CDATA[

說明:fastjson的setAutoTypeSupport特性存在安全漏洞
]]>

  &emsp;對於測試,按照1.2.4.的介紹,則有如下的文件路徑    ![file](https://img2018.cnblogs.com/blog/1812801/201910/1812801-20191025161000622-547325296.jpg)    文件內容比較簡單,這裡就不貼出來了。    &emsp;至此,已經完成了自定義規則的實現,現在就是要把該內容應用到ide上了。首先,需要將該模組進行編譯,這裡直接保存到本地maven參考,好在本地調試。    &emsp;直接將p3c-pmd的版本升級為2.0.1,然後執行mvn install,可以在本地倉庫看到對應的版本    ![file](https://img2018.cnblogs.com/blog/1812801/201910/1812801-20191025161000982-1139222394.jpg)    有了該版本,則可以在其他模組引用該版本進行新功能調試,下面將以idea-plugin模組為例。    #### 3. idea-plugin    &emsp;idea-plugin主要實現了idea的插件,能夠對程式碼進行實時檢查。這裡涉及到idea自定義插件的開發,這裡就不深入了,網上有很多教程。這裡只介紹如何將上面自定義的規則接入該模組。    1.修改idea-plugin模組的build.gradle文件,開啟本地倉庫配置,以便從本地直接載入最新的p3c-pmd依賴。  

buildscript {
repositories {
maven {
url "https://oss.sonatype.org/content/repositories/snapshots/"
}
maven {
url ‘http://dl.bintray.com/jetbrains/intellij-plugin-service’
}
mavenLocal()
mavenCentral()

}  dependencies {      classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"  }

}

allprojects {
group ‘com.alibaba.p3c.idea’
apply plugin: ‘java’
apply plugin: ‘kotlin’
apply plugin: ‘maven-publish’

sourceCompatibility = 1.8  compileJava.options.encoding = 'UTF-8'  configurations.all {      resolutionStrategy.cacheChangingModulesFor 0, 'seconds'  }  repositories {      mavenLocal()      jcenter()      mavenCentral()  }    dependencies {      compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"      testCompile group: 'junit', name: 'junit', version: '4.11'  }

}

  如上,增加了```mavenLocal()```    2.修改p3c-common的build.gradle,更改p3c-pmd的版本為2.0.1    3.修改p3c-common模組resources/rulesets/java/ali=pmd.xml,增加  


“`

以增加自定義規則檢查。

4.在p3c-common模組下,執行gradle clean buildPlugin,生成對應的插件。

4. 驗證

 本地安裝該插件,可以得到如下效果

file

file

 這裡只驗證效果,沒有真正引入fastjson依賴,也驗證了pmd檢查的是源碼文本。

更多原創內容請搜索微信公眾號:啊駝(doubaotaizi)