阿里的pmd项目遵循pmd的规范开发,引用了ast的依赖包做ast树的处理,要在此开源项目基础上做扩展开发首先要明白,静态代码检查受限于ast语法树以及代码结构的原因,很难检测关联性比较强的代码,例如:
类a使用spring注入的方式获取实例,但该bean是在另一个配置类中,此时pmd如果要检测该实例的相关配置,是很难检测的,pmd主要检测静态代码层面,也就是当前类能看到的东西,如果想要去检测类a的实例的某个属性是否符合规范,是很难的。
主要是受限于pmd检测时无法指定检测顺序,且检测时每个类都会进行build maven plugin中所配置的检查项,如果要规范产品线上分布式事务与本地事务的注解问题等,个人认为应该放到测试阶段去检测,可以使用市面常见的混沌测试工具,例如ChaosBlade,对各种可能造成故障的问题代码进行一个故障模拟。
p3c-pmd开发流程如下
1、了解:
官方的pmd项目中默认提供了ast语法树的分析工具
下载地址如下:github.com/pmd/pmd
注意:此工具运行需要JDK10以上及JAVAFX
JAVAFX下载地址如下:download2.gluonhq.com/openjfx/19/...
解压此包之后,添加环境变量:
编辑
编辑
解压pmd-bin包,目录如下:
编辑
点击designer.bat可以启动ast语法分析工具
编辑
中间为源代码,例如我们认为正确的代码,右侧为分析出的ast语法树,从右侧语法树中可以看出相应的节点结构,可以看到有相关的节点例如:
- CompilationUnit根节点
- TypeDeclaration 类信息
- Annotation 接口信息
- MemberValuePairs 接口参数信息
- FieldDeclaration 字段属性信息
- MethodDeclarator 方法相关信息
等等,在编写代码规范规则时,要梳理出正确的代码,并在工具中分析ast语法树,对ast树层层遍历处理校验,即可
2、项目
p3c-pmd结构大体介绍如下:
编辑
以我这边的一个例子来说:
ini
package com.alibaba.p3c.pmd.lang.java.rule.exception;
import com.alibaba.p3c.pmd.lang.java.rule.AbstractAliRule;
import io.seata.spring.annotation.GlobalLock;
import net.sourceforge.pmd.lang.java.ast.*;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.stream.Collectors;
/**
* @classDesc: 功能描述:(检测事务注解是否也加了GlobalLock或GlobalTransaction)
* @author: cy
* @date: 2022/10/11 10:03
*/
public class TransactionMustHaveGlobalLockOrGlobalTransactional extends AbstractAliRule {
private static final Integer ANNOTATION_COUNT = 3;
@Override
public Object visit(ASTCompilationUnit node, Object data) {
//获取包括类注解在内的当前类所有注解
List<ASTAnnotation> allAnnotations = node.findDescendantsOfType(ASTAnnotation.class);
//以下是方法体的节点列表
List<ASTClassOrInterfaceBodyDeclaration> methods = node.findDescendantsOfType(ASTClassOrInterfaceBodyDeclaration.class);
//只保留类注解
List<ASTAnnotation> allClassAnnotations = allAnnotations.stream()
.filter(e -> e.getFirstParentOfType(ASTClassOrInterfaceBodyDeclaration.class) == null)
.collect(Collectors.toList());
//所有类注解类型
List<? extends Class<?>> allClassAnnotationType = allClassAnnotations.stream().map(ASTAnnotation::getType).collect(Collectors.toList());
boolean flag = false;
int count = 0;
for (Class<?> aClass : allClassAnnotationType) {
boolean flagA = aClass == AnnotationConstant.GLOBAL_LOCK.getClassZz();
boolean flagB = aClass == AnnotationConstant.GLOBAL_TRANSACTIONAL.getClassZz();
boolean flagC = aClass == AnnotationConstant.TRANSACTIONAL.getClassZz();
//类是否包含全局注解
if (flagA || flagB || flagC) {
count++;
flag = true;
}
}
if (count == ANNOTATION_COUNT) {
return super.visit(node, data);
}
//遍历所有方法节点
for (ASTClassOrInterfaceBodyDeclaration method : methods) {
//获取方法所有注解
List<ASTAnnotation> methodAnnotation = method.findChildrenOfType(ASTAnnotation.class);
//如果方法上有本地事务注解检查方法上或类上是否有加全局事务或锁注解
List<ASTAnnotation> collect = methodAnnotation.stream().filter(e -> e.getType().equals(AnnotationConstant.TRANSACTIONAL.getClassZz())).collect(Collectors.toList());
if (!collect.isEmpty()) {
//方法上全局事务或全局锁注解列表
List<ASTAnnotation> lockOrTransactionAnnotations = methodAnnotation.stream().filter(e -> e.getType().equals(AnnotationConstant.GLOBAL_TRANSACTIONAL.getClassZz()) ||
e.getType().equals(AnnotationConstant.GLOBAL_LOCK.getClassZz())).collect(Collectors.toList());
//方法注解不包含@GlobalLock or @GlobalTransaction 且类注解也不包含
if (lockOrTransactionAnnotations.isEmpty() && !flag) {
ASTMethodDeclaration methodNode = method.getFirstDescendantOfType(ASTMethodDeclaration.class);
addViolationWithMessage(data, methodNode,
"java.exception.MethodTransactionMustHaveGlobalLockOrGlobalTransactionRule.msg",
new Object[]{methodNode.getMethodName()});
}
}
}
return super.visit(node, data);
}
}
所有规则类继承了AbstractAliRule,AbstractAliRule继承了AbstractJavaRule,在往上继承了JavaParserVisitor接口,AbstractJavaRule实现了JavaParserVisitor接口所定义的一系列访问ast树的方法,全部提供了一种默认的访问检查方式,同时提供给子类继承重写的能力,AbstractAliRule再次做了一次封装,此方式看起来是访问者模式(没仔细看,应该是)
例如我们要实现对类的根节点开始处理,我们就实现:
typescript
@Override
public Object visit(ASTCompilationUnit node, Object data) {
return super.visit(node, data);
}
可以看到入参为:ASTCompilationUnit类型,对比ast语法树可以发现为根节点,以此为根节点向下遍历处理即可,同时
编辑
net.sourceforge.pmd.lang.java.ast
该包下提供了大量的ast树节点,选择相应的节点树遍历即可。
同时,选择节点还可以通过xpath语法来获取ast树节点,例如:
编辑
通过xpath语法可以获取到相应的节点
同时xpath也支持正则表达式
具体写法可以看上方代码示例
规则写完之后要在xml中配置相应xpath规则指向
xml
<rule name="TransactionMustHaveGlobalLockOrGlobalTransactional"
language="java"
since="1.6"
message="java.exception.MethodTransactionMustHaveGlobalLockOrGlobalTransactionRule.msg"
class="com.alibaba.p3c.pmd.lang.java.rule.exception.TransactionMustHaveGlobalLockOrGlobalTransactional">
<priority>1</priority>
<example>
<![CDATA[
public int method() {
Integer a = null;
return a;
}
]]>
</example>
</rule>
主要是配置name规则名,message指向,处理类class
example中可以填写正确规范的代码
同时message指向key为message.xml中的键
配置如下:
xml
<entry key="java.exception.MethodTransactionMustHaveGlobalLockOrGlobalTransactionRule.msg">
<![CDATA[方法【%s】本地事务注解未加全局事务@GlobalLock或全局锁@GlobalTransactional]]>
</entry>
发送消息可以使用:
javascript
addViolationWithMessage(data, methodNode,
"java.exception.MethodTransactionMustHaveGlobalLockOrGlobalTransactionRule.msg",
new Object[]{methodNode.getMethodName()});
指定相应的key和占位符替换内容
3、测试类书写
scala
public class OopRuleTest extends SimpleAggregatorTst {
// 加载CLASSPATH下的rulesets/java/ali-oop.xml
private static final String RULESET = "java-ali-oop";
@Override
public void setUp() {
addRule(RULESET, "EqualsAvoidNullRule");
addRule(RULESET, "WrapperTypeEqualityRule");
addRule(RULESET, "PojoNoDefaultValueRule");
addRule(RULESET, "PojoMustUsePrimitiveFieldRule");
addRule(RULESET, "PojoMustOverrideToStringRule");
addRule(RULESET, "StringConcatRule");
addRule(RULESET, "BigDecimalAvoidDoubleConstructorRule");
addRule(RULESET, "AbstractMsunRule");
}
}
通过继承SimpleAggregatorTst并使用addRule(RULESET, "AbstractMsunRule")添加测试规则
-
添加测试数据:(在测试类的resources文件下的如图所示的xml中配置)
编辑
xml
<!-- 示例代码-->
<code-fragment id="class-without-comments"><![CDATA[
public class CommentsMustBeJavadocFormat {
private String name;
public void getName() {}
}
]]>
</code-fragment>
<test-code>
<!-- 描述-->
<description>Class have no comment.</description>
<!-- 忽略 0 一个也不忽略-->
<expected-problems>0</expected-problems>
<!-- 对应的code-fragment id-->
<code-ref id="class-without-comments" />
</test-code>
4、打包
pom中添加如下配置,跳过测试和javadoc生成
vbnet
<properties>
<maven.test.skip>true</maven.test.skip>
<maven.javadoc.skip>true</maven.javadoc.skip>
</properties>
mvn install 即可