阿里很早之前出过一个流传在坊间的《Java开发手册.pdf》,这个里面定义了很多开发规范,其中最基本必须要遵守的规范阿里出了一套Sonar插件来帮助扫描
说的开发插件其实本质就是利用源设计者留下的入口拿到源事件然后进行实现,最终你的实现会在你不知道的时间,不知道的地点被调度执行,这就是设计模式的魅力,保证了开闭原则下进行了功能扩展
那么Sonar的设计者为我们留下了哪些入口呢?
这个时候我们经常会百度、谷歌,一个小时过去后你会发现还是啥都没明白,因为资料太少并且太散,所以你才需要这篇博客~~~
言归正传,Sonar留给我们的扩展口那可是相当的多,各位看官先别急,我们先了解下Sonar解析完我们java文件后生成的是什么
AST语法树
AST(Abstract Syntax Tree)又叫抽象语法树,Sonar会将我们的java文件解析成AST语法树,那么什么是AST语法树呢?其实后端开发人员可能不太了解,但是前端的话会比较熟悉
public class TimeCase2 {
public void test(){
String s = "";
Long start = System.currentTimeMillis();
}
}
这样一段Java代码会生成如下AST树结构
整个AST语法树实在太庞大了,无法一一画出,吧主要核心部分都画出来了,其实每个树枝节点在Sonar中对应的实现类的作用通过图解就能大致明白了,其实AST树各个节点都没有限制类型,主要是根据代码的表达式、行为作出不一样的树节点:描述类型、行为类型、表达式类型、返回值类型等等树节点
类 | 作用 |
Tree | 整个Class类的根节点,类的元数据信息 |
ModifierKeywordTreeImpl | 类的描述、注解、名字等等信息 |
InternalSyntaxToken | 记录类开始符号、结束符号,适配各种语法 |
MethodTreeImpl | 方法实现 记录方法的名字、描述、注解、入参、出参等等.... |
BlockTreeImpl | 方法内部代码块实现 匹配内部各行代码记录 |
其实还有很多实现类型,他们的顶级interface都是Tree接口,这个顶部接口定义了如下接口方法:
//判断当前具体实现类的类型
boolean is(Tree.Kind... var1);
//函数执行
void accept(TreeVisitor var1);
//当前节点的父节点
@Nullable
Tree parent();
//当前节点开始符号
@Nullable
SyntaxToken firstToken();
//当前节点结束符号
@Nullable
SyntaxToken lastToken();
//当前节点类型
Tree.Kind kind();
那么Tree这个顶部接口有多少子类接口继承了呢?非常的多,每种子类接口都再次扩展了新的功能:
COMPILATION_UNIT(CompilationUnitTree.class),
CLASS(ClassTree.class),
ENUM(ClassTree.class),
INTERFACE(ClassTree.class),
ANNOTATION_TYPE(ClassTree.class),
ENUM_CONSTANT(EnumConstantTree.class),
INITIALIZER(BlockTree.class),
STATIC_INITIALIZER(StaticInitializerTree.class),
CONSTRUCTOR(MethodTree.class),
METHOD(MethodTree.class),
BLOCK(BlockTree.class),
EMPTY_STATEMENT(EmptyStatementTree.class),
LABELED_STATEMENT(LabeledStatementTree.class),
EXPRESSION_STATEMENT(ExpressionStatementTree.class),
IF_STATEMENT(IfStatementTree.class),
ASSERT_STATEMENT(AssertStatementTree.class),
SWITCH_STATEMENT(SwitchStatementTree.class),
CASE_GROUP(CaseGroupTree.class),
CASE_LABEL(CaseLabelTree.class),
WHILE_STATEMENT(WhileStatementTree.class),
DO_STATEMENT(DoWhileStatementTree.class),
FOR_STATEMENT(ForStatementTree.class),
FOR_EACH_STATEMENT(ForEachStatement.class),
BREAK_STATEMENT(BreakStatementTree.class),
CONTINUE_STATEMENT(ContinueStatementTree.class),
RETURN_STATEMENT(ReturnStatementTree.class),
THROW_STATEMENT(ThrowStatementTree.class),
SYNCHRONIZED_STATEMENT(SynchronizedStatementTree.class),
TRY_STATEMENT(TryStatementTree.class),
CATCH(CatchTree.class),
POSTFIX_INCREMENT(UnaryExpressionTree.class),
POSTFIX_DECREMENT(UnaryExpressionTree.class),
PREFIX_INCREMENT(UnaryExpressionTree.class),
PREFIX_DECREMENT(UnaryExpressionTree.class),
UNARY_PLUS(UnaryExpressionTree.class),
UNARY_MINUS(UnaryExpressionTree.class),
BITWISE_COMPLEMENT(UnaryExpressionTree.class),
LOGICAL_COMPLEMENT(UnaryExpressionTree.class),
MULTIPLY(BinaryExpressionTree.class),
DIVIDE(BinaryExpressionTree.class),
REMAINDER(BinaryExpressionTree.class),
PLUS(BinaryExpressionTree.class),
MINUS(BinaryExpressionTree.class),
LEFT_SHIFT(BinaryExpressionTree.class),
RIGHT_SHIFT(BinaryExpressionTree.class),
UNSIGNED_RIGHT_SHIFT(BinaryExpressionTree.class),
LESS_THAN(BinaryExpressionTree.class),
GREATER_THAN(BinaryExpressionTree.class),
LESS_THAN_OR_EQUAL_TO(BinaryExpressionTree.class),
GREATER_THAN_OR_EQUAL_TO(BinaryExpressionTree.class),
EQUAL_TO(BinaryExpressionTree.class),
NOT_EQUAL_TO(BinaryExpressionTree.class),
AND(BinaryExpressionTree.class),
XOR(BinaryExpressionTree.class),
OR(BinaryExpressionTree.class),
CONDITIONAL_AND(BinaryExpressionTree.class),
CONDITIONAL_OR(BinaryExpressionTree.class),
CONDITIONAL_EXPRESSION(ConditionalExpressionTree.class),
ARRAY_ACCESS_EXPRESSION(ArrayAccessExpressionTree.class),
MEMBER_SELECT(MemberSelectExpressionTree.class),
NEW_CLASS(NewClassTree.class),
NEW_ARRAY(NewArrayTree.class),
METHOD_INVOCATION(MethodInvocationTree.class),
TYPE_CAST(TypeCastTree.class),
INSTANCE_OF(InstanceOfTree.class),
PARENTHESIZED_EXPRESSION(ParenthesizedTree.class),
ASSIGNMENT(AssignmentExpressionTree.class),
MULTIPLY_ASSIGNMENT(AssignmentExpressionTree.class),
DIVIDE_ASSIGNMENT(AssignmentExpressionTree.class),
REMAINDER_ASSIGNMENT(AssignmentExpressionTree.class),
PLUS_ASSIGNMENT(AssignmentExpressionTree.class),
MINUS_ASSIGNMENT(AssignmentExpressionTree.class),
LEFT_SHIFT_ASSIGNMENT(AssignmentExpressionTree.class),
RIGHT_SHIFT_ASSIGNMENT(AssignmentExpressionTree.class),
UNSIGNED_RIGHT_SHIFT_ASSIGNMENT(AssignmentExpressionTree.class),
AND_ASSIGNMENT(AssignmentExpressionTree.class),
XOR_ASSIGNMENT(AssignmentExpressionTree.class),
OR_ASSIGNMENT(AssignmentExpressionTree.class),
INT_LITERAL(LiteralTree.class),
LONG_LITERAL(LiteralTree.class),
FLOAT_LITERAL(LiteralTree.class),
DOUBLE_LITERAL(LiteralTree.class),
BOOLEAN_LITERAL(LiteralTree.class),
CHAR_LITERAL(LiteralTree.class),
STRING_LITERAL(LiteralTree.class),
NULL_LITERAL(LiteralTree.class),
IDENTIFIER(IdentifierTree.class),
VARIABLE(VariableTree.class),
ARRAY_TYPE(ArrayTypeTree.class),
PARAMETERIZED_TYPE(ParameterizedTypeTree.class),
UNION_TYPE(UnionTypeTree.class),
UNBOUNDED_WILDCARD(WildcardTree.class),
EXTENDS_WILDCARD(WildcardTree.class),
SUPER_WILDCARD(WildcardTree.class),
ANNOTATION(AnnotationTree.class),
MODIFIERS(ModifiersTree.class),
LAMBDA_EXPRESSION(LambdaExpressionTree.class),
PRIMITIVE_TYPE(PrimitiveTypeTree.class),
TYPE_PARAMETER(TypeParameterTree.class),
IMPORT(ImportTree.class),
PACKAGE(PackageDeclarationTree.class),
ARRAY_DIMENSION(ArrayDimensionTree.class),
OTHER(Tree.class),
TOKEN(SyntaxToken.class),
TRIVIA(SyntaxTrivia.class),
INFERED_TYPE(InferedTypeTree.class),
TYPE_ARGUMENTS(TypeArguments.class),
METHOD_REFERENCE(MethodReferenceTree.class),
TYPE_PARAMETERS(TypeParameters.class),
ARGUMENTS(Arguments.class),
LIST(ListTree.class);
这个枚举类就列出了所有Tree接口的子类接口定义,不同子类接口都对应了代码不同的表达式、描述等等实现,所以我们之后再扩展sonar插件时针对我们需要扩展的规则类型可能扫描的代码,就可以从上面的超多子类实现中直接获取我们要的信息,而不需要我们自己从顶部Tree一个个循环获取,请继续看下文解析~~
Sonar存储核心表结构
Sonar默认使用的是PGSQL
表名 | 描述 |
projects | 表保存了所有被sonar分析过的项目的基本信息 |
metrics | 此表保存的是测试指标,比如测试覆盖率,代码复杂度等等 |
rules_profiles | 所有的测试指标存储在metrics表中。rules_profiles这个表保存的就是这些metrics的一个子集,可以认为是定制化的测试标准集合。每个project都会有相应的rules_project与之对应 |
snapshots | 有了projects,有了rules_profiles。按照某种rules_profile对某个project进行一次sonar分析,就会产生一些snapshots。但是这里其实并没有存储真正的分析出来的指标值。而是存放在project_measures这个表中。snapshots和project_measures通过外键关联 |
开发Sonar插件
新建一个maven项目,然后POM修改
<?xml version="1.0" encoding="UTF-8"?>
<project>
...
<build>
<plugins>
<plugin>
<groupId>org.sonarsource.sonar-packaging-maven-plugin</groupId>
<artifactId>sonar-packaging-maven-plugin</artifactId>
<extensions>true</extensions>
<configuration>
<pluginClass>org.sonarqube.plugins.example.ExamplePlugin</pluginClass>
</configuration>
</plugin>
</plugins>
</build>
</project>
实现Sonar的Plugin接口
public class MyJavaRulesPlugin implements Plugin {
@Override
public void define(Context context) {
// server extensions -> objects are instantiated during server startup
context.addExtension(MyJavaRulesDefinition.class);
// batch extensions -> objects are instantiated during code analysis
context.addExtension(MyJavaFileCheckRegistrar.class);
}
}
- context.addExtension这个是添加扩展执行类,MyJavaRulesDefinition实现RulesDefinition进行一些规则元数据信息的描述,主要是对自己自定义规则的描述
public class MyJavaRulesDefinition implements RulesDefinition {
public static final String REPOSITORY_KEY = "finger-java-custom-rules";
private final Gson gson = new Gson();
@Override
public void define(Context context) {
NewRepository repository = context
.createRepository( REPOSITORY_KEY, Java.KEY )
.setName( "Finger Java Custom Rules" );
new AnnotationBasedRulesDefinition( repository, Java.KEY )
.addRuleClasses(/* don't fail if no SQALE annotations */ false, RulesList.getChecks() );
for (NewRule rule : repository.rules()) {
String metadataKey = rule.key();
System.out.println( metadataKey );
// Setting internal key is essential for rule templates (see SONAR-6162), and it is not done by AnnotationBasedRulesDefinition from
// sslr-squid-bridge version 2.5.1:
rule.setInternalKey( metadataKey );
rule.setHtmlDescription( readRuleDefinitionResource( metadataKey + ".html" ) );
addMetadata( rule, metadataKey );
}
repository.done();
}
@Nullable
private static String readRuleDefinitionResource(String fileName) {
URL resource = MyJavaRulesDefinition.class.getResource( "/org/sonar/l10n/java/rules/squid/" + fileName );
if (resource == null) {
return null;
}
try {
return Resources.toString( resource, Charsets.UTF_8 );
} catch (IOException e) {
throw new IllegalStateException( "Failed to read: " + resource, e );
}
}
private void addMetadata(NewRule rule, String metadataKey) {
String json = readRuleDefinitionResource( metadataKey + ".json" );
if (json != null) {
RuleMetadata metadata = gson.fromJson( json, RuleMetadata.class );
rule.setSeverity( metadata.defaultSeverity.toUpperCase( Locale.US ) );
rule.setName( metadata.title );
rule.setTags( metadata.tags );
rule.setStatus( RuleStatus.valueOf( metadata.status.toUpperCase( Locale.US ) ) );
if (metadata.remediation != null) {
// metadata.remediation is null for template rules
rule.setDebtRemediationFunction( metadata.remediation.remediationFunction( rule.debtRemediationFunctions() ) );
rule.setGapDescription( metadata.remediation.linearDesc );
}
}
}
private static class RuleMetadata {
String title;
String status;
@Nullable
Remediation remediation;
String[] tags;
String defaultSeverity;
}
private static class Remediation {
String func;
String constantCost;
String linearDesc;
String linearOffset;
String linearFactor;
private DebtRemediationFunction remediationFunction(DebtRemediationFunctions drf) {
if (func.startsWith( "Constant" )) {
return drf.constantPerIssue( constantCost.replace( "mn", "min" ) );
}
if ("Linear".equals( func )) {
return drf.linear( linearFactor.replace( "mn", "min" ) );
}
return drf.linearWithOffset( linearFactor.replace( "mn", "min" ), linearOffset.replace( "mn", "min" ) );
}
}
}
- MyJavaFileCheckRegistrar这个就是主要扩展规则类,类似责任类模式,将需要执行的扩展类添加进去即可
@SonarLintSide
public class MyJavaFileCheckRegistrar implements CheckRegistrar {
/**
* Register the classes that will be used to instantiate checks during analysis.
*/
@Override
public void register(RegistrarContext registrarContext) {
// Call to registerClassesForRepository to associate the classes with the correct repository key
registrarContext.registerClassesForRepository(MyJavaRulesDefinition.REPOSITORY_KEY, Arrays.asList(checkClasses()), Arrays.asList(testCheckClasses()));
}
/**
* Lists all the checks provided by the plugin
*/
public static Class<? extends JavaCheck>[] checkClasses() {
return new Class[] {
xx.class, //填入你扩展规则类即可
xx1.class
};
}
/**
* Lists all the test checks provided by the plugin
*/
public static Class<? extends JavaCheck>[] testCheckClasses() {
return new Class[] {};
}
}
具体规则扩展类开发
上面已经正确的接入了Sonar的扩展,接下去就是实现具体的扩展了
/**
* <p>Title:线程池规范校验</p>
* <p>Description:</p>
*
* @author QIQI
* @params
* @return
* @throws
* @date 2021/06/10 17:26
*/
@Rule(key = "ThreadPoolCheck")
public class ThreadPoolCheck extends BaseTreeVisitor implements JavaFileScanner {
private static final Logger LOGGER = LoggerFactory.getLogger( ThreadPoolCheck.class );
private JavaFileScannerContext context;
@Override
public void scanFile(JavaFileScannerContext context) {
this.context = context;
scan( context.getTree() );
}
@Override
public void visitBlock(BlockTree tree) {
List<StatementTree> statementTrees = tree.body();
if (statementTrees != null && statementTrees.size() > 0) {
LOGGER.info( "ThreadPoolCheck start ...." );
for (StatementTree tree1 : statementTrees) {
if (tree1 instanceof VariableTreeImpl) {
CompletableFuture.runAsync( () -> System.out.println(1) );
executorServiceCheck( (VariableTreeImpl) tree1 );
} else if (tree1 instanceof ExpressionStatementTreeImpl) {
completableFutureCheck( (ExpressionStatementTreeImpl) tree1 );
}
}
}
super.visitBlock( tree );
}
@Override
public void visitAnnotation(AnnotationTree annotationTree) {
if(annotationTree.annotationType().toString().equals( "Async" )){
if(annotationTree.arguments().size() == 0){
context.reportIssue( this, annotationTree, "Unified thread pool is used except for special requirement thread pool" );
}
}
super.visitAnnotation( annotationTree );
}
private void executorServiceCheck(VariableTreeImpl variableTree) {
if (variableTree.type().toString().equals( "ExecutorService" )) {
List<Tree> treeList = variableTree.getChildren();
if (treeList != null && treeList.size() > 0) {
for (Tree tree2 : treeList) {
if (tree2 instanceof MethodInvocationTreeImpl) {
MethodInvocationTreeImpl methodInvocationTree = (MethodInvocationTreeImpl) tree2;
if (methodInvocationTree.symbol().name().equals( "newFixedThreadPool" )) {
context.reportIssue( this, methodInvocationTree, "Unified thread pool is used except for special requirement thread pool" );
}
}
}
}
}
}
private void completableFutureCheck(ExpressionStatementTreeImpl expressionStatementTree) {
MethodInvocationTreeImpl methodInvocationTree = (MethodInvocationTreeImpl) expressionStatementTree.expression();
if(methodInvocationTree != null && methodInvocationTree.arguments().size() < 2){
context.reportIssue( this, expressionStatementTree, "Unified thread pool is used except for special requirement thread pool" );
}
}
}
- 固定继承BaseTreeVisitor,实现JavaFileScanner
- 接下去的就看你需要@Override那个方法了,整个父类有非常多的可以@Override,不同的@Override对应不同的规则校验场景,Sonar会把不同的Tree子类实现扔到不同@Override的实现类中的参数传递进来
- 我现在需要校验方法代码块中代码校验,所以我@Override了visitBlock实现,Sonar就传进来了BlockTree子类接口
- 我现在需要校验方法注解,所以我又@Override了visitAnnotation,Sonar就传进来了AnnotationTree的子类接口
- 同一个扩展类中可以@Override多个,根据场景需要来开发即可
各位同学还没结束~~~~
我们现在只是开发完了规则,还没做案例展示呢~,我们平时被扫描出BUG后,不是都有案例告诉你怎么修改的嘛,所以我们也需要定义案例
细心的同学已经发现了,上面的代码有个注解@Rule(key = "ThreadPoolCheck"),这个名字必须要对应下面案例文件的名字
- ThreadPoolCheck.html
<p>MachineTimeCheck Check</p>
<h2>Noncompliant Code Example</h2>
<pre>
Executors.newFixedThreadPool();
CompletableFuture.runAsync( () -> System.out.println(1) );
@Async;
</pre>
<h2>Compliant Solution</h2>
<pre>
ThreadFactory.getThreadExecutor( APOLLO-CONFIG );
CompletableFuture.runAsync( () -> System.out.println(1), ThreadFactory.getThreadExecutor( APOLLO-CONFIG ) );
(https://duapp.yuque.com/team_tech/confluence-data-iwskfg/ul9gg1)
</pre>
- ThreadPoolCheck.json(这个文件代表扫描到之后对于问题的定位:异味?BUG?漏洞?)
{
"title": "Unified thread pool is used except for special requirement thread pool",
"status": "ready",
"remediation": {
"func": "Constant\/Issue",
"constantCost": "5min"
},
"tags": [
"bug",
"pitfall"
],
"defaultSeverity": "CRITICAL"
}
- ThreadPoolCheck_java.json
{
"title": "Unified thread pool is used except for special requirement thread pool",
"status": "ready",
"remediation": {
"func": "Constant\/Issue",
"constantCost": "5min"
},
"tags": [
"bug",
"pitfall"
],
"defaultSeverity": "CRITICAL"
}
最后插件的测试用例怎么写?
test下新建一个files文件夹,新建一个ThreadPoolCase.java文件,里面写一个不符合规则的反例
/**
* <p>Title:</p>
* <p>Description:</p>
* @author QIQI
* @params
* @return
* @throws
* @date 2021/06/10 18:11
*/
public class ThreadPoolCase {
public void test(){
ExecutorService executorService = Executors.newFixedThreadPool( 1 ); // Noncompliant
}
}
然后新建测试用例
@Test
public void test() {
JavaCheckVerifier.verify("src/test/files/ThreadPoolCase.java", new ThreadPoolCheck());
}