sonar-scanner的執行流程和對ClassLoader,動態代理的使用

最近項目上使用了sonarqube來提供靜態代碼檢查的服務,在看sonar-scanner的源碼的時候,發現sonar-scanner用來分析的jar包是從sonar的服務器上下載下來的,使用自定義的ClassLoader來加載這些從服務器上下載下來的jar包,然後使用了jdk的動態代理來創建了一個啓動器類,然後使用這個啓動器調用了sonar提供的Batch API啓動了代碼分析

Sonar的scanner中對ClassLoader和JDK的動態代理的使用,是ClassLoader的一個比較典型的應用場景,本文會以sonar-scanner的源碼分析來說明soanr-scanner是如何使用ClassLoader和JDK的動態代理的

sonar的scanner是如何啓動的

不管是soanr-scanner的客戶端,還是maven插件,gradle插件sonar-scanner執行分析的方式都是調用了sonar-scanner-api這個jar包中的類,創建了一個EmbeddedScanner來執行分析的,如果我們手動調用的話,代碼大概是這樣的:

package com.jiaoyiping.baseproject.sonar;

import org.sonarsource.scanner.api.EmbeddedScanner;
import org.sonarsource.scanner.api.LogOutput;
import org.sonarsource.scanner.api.ScanProperties;
import org.sonarsource.scanner.api.StdOutLogOutput;

import java.util.LinkedHashMap;
import java.util.Map;

/**
 * Created with Intellij IDEA
 *
 * @author: jiaoyiping
 * Mail: [email protected]
 * Date: 2018/08/02
 * Time: 21:49
 * To change this template use File | Settings | Editor | File and Code Templates
 */

public class SonarScannerDemo {

private static LogOutput logOutput = new StdOutLogOutput();

//sonar的配置
private static Map<String, String> sonarPropertiesMap = new LinkedHashMap<String, String>() {{
    put("sonar.host.url", "http://192.168.1.101:9000/sonar");
    put("sonar.sourceEncoding", "UTF-8");
    put("sonar.login", "xxxxxxxx8ca15ed386d08ffac90ad4efdb9a3");

}};

//項目代碼的配置
private static Map<String, String> projectSettingMap = new LinkedHashMap<String, String>() {{
    put(ScanProperties.PROJECT_KEY, "abcdef");
    put(ScanProperties.PROJECT_BASEDIR, "D:\\temp\\stateless4j");
    put(ScanProperties.PROJECT_SOURCE_DIRS, "src\\main\\java");
    put(ScanProperties.PROJECT_SOURCE_ENCODING, "UTF-8");
    put("sonar.java.binaries", "target\\classes");
    put("sonar.java.source", "src\\main\\java");

}};

public static void main(String[] args) {
    //使用sonar的分析器來分析代碼
    //規則從服務器下載
    //分析的結果再上傳到服務器上去

    EmbeddedScanner scanner = EmbeddedScanner.create("Gradle", "6.7.4", logOutput);
    scanner.addGlobalProperties(sonarPropertiesMap);
    scanner.start();
    scanner.execute(projectSettingMap);


    }
}    

創建了一個EmbeddedScanner,然後設置全局屬性,然後調用這個scanner的start方法,然後傳入項目相關的一些屬性來執行分析

其中start方法的作用是使用上邊的全局屬性中的soanr服務器的信息,從服務器上下載相關的jar包,並使用JDK的動態代理來創建相應的啓動器對象,所以,這一部分是我們主要要看的(soanr執行分析的部分,涉及到了一個在sonar中很重要的設計模式,就是visitor模式,這裏不進行分析,以後的文章會分析這一部分)

在EmbeddedScanner的create工廠方法裏,創建了IsolatedLauncherFactory的實例,在IsolatedLauncherFactory 的createLauncher方法中,執行了下載jar包和使用動態代理創建launcher的方法

接下來,我們看jarDownloader是如何下載jar包的:

getbootstrapIndexDownloader.getIndex()會去獲取可以下載的jar包的名稱和hash值:

我們用瀏覽器來調用這個地址,可以看到返回的內容就是jar包的名稱和hash值

fileCache.get()方法會調用ScannerFileDownloader的download方法將jar包下載下來(參考上邊的代碼截圖)

下載完jar包之後,就是根據下載的jar包來創建一個classLoader,其實就是創建了一個自定義的繼承了UrlClassLoader的IsolatedClassloader,然後把我們下載下來的jar包轉化爲url,添加到calssLoader裏邊去,這樣,我們從這個classLoader來加載對應的類的時候,就能加載到我們下載下來的jar包中的類(ClassLoader工作的方法是首先讓父類去加載,父類加載不到,拋出異常的時候,再嘗試調用自己的findClass去加載,但是sonar-scanner中的ClassLoader的實現偷了一個懶,直接將url添加到父類UrlClassLoader的url列表裏去了,但是加載的效果是一樣的)

以下是IsolatedLauncherFactory的 createClassLoader方法

接下來就是調用IsolatedLauncherProxy這個類的create方法來生成launcher了
IsolatedLauncherFactory的createLauncher方法調用了這個類,使用了我們剛纔生成的ClassLoader

cl = createClassLoader(jarFiles, rules);
IsolatedLauncher objProxy = IsolatedLauncherProxy.create(cl, IsolatedLauncher.class, launcherImplClassName, logger);

launcherImplClassName通過定義在IsolatedLauncherFactory中的一個字符串常量,在構造方法中設置的

IsolatedLauncherProxy的實現代碼如下,是一個典型的使用JDK的動態代理的代碼,通過傳入的ClassLoader,和要生成的類的名稱org.sonarsource.scanner.api.internal.batch.BatchIsolatedLauncher,我們生成了BatchIsolatedLauncher的實例,這個BatchIsolatedLauncher調用了sonar提供的Batch類,傳入相應的參數,實現了代碼的靜態檢查這個功能:

package org.sonarsource.scanner.api.internal;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;
import org.sonarsource.scanner.api.internal.cache.Logger;

public class IsolatedLauncherProxy implements InvocationHandler {
  private final Object proxied;
  private final ClassLoader cl;
  private final Logger logger;

  private IsolatedLauncherProxy(ClassLoader cl, Object proxied, Logger logger) {
    this.cl = cl;
    this.proxied = proxied;
    this.logger = logger;
  }

  public static <T> T create(ClassLoader cl, Class<T> interfaceClass, String proxiedClassName, Logger logger) throws ReflectiveOperationException {
    Object proxied = createProxiedObject(cl, proxiedClassName);
    // interfaceClass needs to be loaded with a parent ClassLoader (common to both ClassLoaders)
    // In addition, Proxy.newProxyInstance checks if the target ClassLoader sees the same class as the one given
    Class<?> loadedInterfaceClass = cl.loadClass(interfaceClass.getName());
    return (T) create(cl, proxied, loadedInterfaceClass, logger);
  }

  public static <T> T create(ClassLoader cl, Object proxied, Class<T> interfaceClass, Logger logger) {
    Class<?>[] c = {interfaceClass};
    return (T) Proxy.newProxyInstance(cl, c, new IsolatedLauncherProxy(cl, proxied, logger));
  }

  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    ClassLoader initialContextClassLoader = Thread.currentThread().getContextClassLoader();

    try {
      Thread.currentThread().setContextClassLoader(cl);
      logger.debug("Execution " + method.getName());
      return method.invoke(proxied, args);
    } catch (UndeclaredThrowableException | InvocationTargetException e) {
      throw unwrapException(e);
    } finally {
      Thread.currentThread().setContextClassLoader(initialContextClassLoader);
    }
  }

  private static Throwable unwrapException(Throwable e) {
    Throwable cause = e;

    while (cause.getCause() != null) {
      if (cause instanceof UndeclaredThrowableException || cause instanceof InvocationTargetException) {
        cause = cause.getCause();
      } else {
        break;
      }
    }
    return cause;
  }

  private static Object createProxiedObject(ClassLoader cl, String proxiedClassName) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
    Class<?> proxiedClass = cl.loadClass(proxiedClassName);
    return proxiedClass.newInstance();
  }
}

整個soanr-scanner的啓動的流程就是上邊寫到的這些東西,設計的很巧妙,我們客戶端使用的時候,只需要依賴一個 sonar-scanner-api即可,分析代碼需要的jar包,會從sonar的服務器上去下載,然後本地分析完成之後的結果,又會上傳到服務器上去進行圖表展示,這樣我們自己寫的分析規則,只要上傳到服務器上去即可,真正執行分析的時候,sonar-scanner會從服務器上去下載,因爲有一部分jar包要到服務器上去下載,而這些jar包又是不固定的,有可能會變化,這樣,使用一個自定義的ClassLoader來加載這些jar包就是很自然的事情了

而使用JDK的動態代理,我們不僅創建了一個使用了之前的ClasLloader加載的類的對象,而且在這個對象的方法執行前設置了ContextClassLoader,在方法執行後,又將之前的CalssLoader給還原回來

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章