最近項目上使用了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: jiaoyiping@gmail.com
* 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給還原回來