Sonar-扫描插件自定义扩展

 

阿里很早之前出过一个流传在坊间的《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());
    }
  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

菠萝-琪琪

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值