Gradle 生态系统源码分析

Gradle 进阶 第五篇

虽千万人,吾往矣

Plugin 应用

接着上一节所讲的微内核架构,系统的 Plugin 管理简单的实现方式就是通过在系统内部实现一个注册表,用来获取 Plugin,并且得到 Plugin 的可用性。
下面来从源码展开了解一下 Gradle plugin 的管理。在源码的110多个模块中,plugin 管理先关的有三个,分别是 gradle.plugin-development、gradle.plugins、以及 gradle.plugin-user。
其中 gradle.plugin-development 是关于支持开发 plugin 的模块,
在后面“如何设计 Plugin”中将会详细讲解。这里举个简单例子:JavaGradlePluginPlugin,它就是我们平常写 plugin 时需要 apply 的 plugin,摘个官网的例子:

plugins {
    id 'java-gradle-plugin'
}

gradlePlugin {
    plugins {
        simplePlugin {
            id = 'org.samples.greeting'
            implementationClass = 'org.gradle.GreetingPlugin'
        }
    }
}

gradle.plugins 模块中定义了大部分的内置 Plugins,这里就一句带过,后文会有一系列专门的文章来介绍一些特别的 Plugin。
本文的重点是系统如何管理 plugin,也就是模块 gradle.plugin-user的主要功能。
一般在 Gradle 的使用中,apply 一个 plugin 都是通过在脚本文件中声明来完成的,就如同上面的代码片中所示,apply 了"java-gradle-plugin"。Gradle 也提供了通过 java 代码来 apply 一个 plugin,代码如下:

 //invoke side
 project.getPluginManager().apply(BasePlugin.class);
 ==========================
// in DefaultPluginManager
    public void apply(String pluginId) {
        PluginImplementation<?> plugin = pluginRegistry.lookup(DefaultPluginId.unvalidated(pluginId));
        if (plugin == null) {
            throw new UnknownPluginException("Plugin with id '" + pluginId + "' not found.");
        }
        doApply(plugin);
    }

除了和 Gradle 一起发布的内部 plugin 之外,我们也可自定义一些本地的 Plugin,或者使用一些远程的第三方 plugin。

  1. 应用 buildSrc 目录下的 Plugin
  2. 应用脚本里直接编写的 Plugin
  3. 应用本地/远程仓库里的 Plugin

这里就不做详细的demo,下面分析源码的时候会有提及。

plugin 查找

之前的一篇文章中讲解了有关 Gradle 脚本的编译过程,其中有一个细节省略未说,就是在编译每一个 .gradle 文件的时候都会分为两部分,如果脚本文件里有 buildscript{} 代码块,就会先编译 buildscript{} 里的代码、或者plugins{} 为一个 class,编译完成之后直接运行,接着编译剩下的代码为一个 class,再运行。注意这里有一点需要强调,apply plugin:"xxx" 和 plugins{} 不一样,apply plugin:"xxx" 并不会在第一部分。
举个简单的例子:

// case 1
plugins {
    id 'com.android.application'
}
======= 分割线 ======
android {
    compileSdkVersion 30
    buildToolsVersion "30.0.2"

    defaultConfig {
      ...
    }
}
// case 2
buildscript {
    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath "com.android.tools.build:gradle:4.1.0"
    }
}
======= 分割线 ======
allprojects {
    repositories {
        google()
        jcenter()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

在分割线之上的会先编译,并且运行,之后编译分割线之下的部分,再运行。其中 plugins {} 的代码块就是用来声明所需要的 Plugin 的一个Request。Gradle 会根据这个 PluginRequest 去查找需要加载的 plugin。

这里需要注意一点,case 2 中的 classpath "com.android.tools.build:gradle:4.1.0",已经将 android plugin 加入了 dependencies 中,Gradle 就会去指定的仓库中去下载相应的插件版本并且加入 ClassPath 的 scorp 中。所以对于"应用本地/远程仓库里的 Plugin",都需要 buildscript 的 dependencies 中声明。

关于 Gradle 是如何通过脚本把 Plugin 加入 Runtime,我先上一幅类图,作以梳理:


PluginManager.png

在 DefaultScriptPluginFactory 类中:

            // Pass 1, extract plugin requests and plugin repositories and execute buildscript {}, ignoring (i.e. not even compiling) anything else

            CompileOperation<?> initialOperation = compileOperationFactory.getPluginsBlockCompileOperation(initialPassScriptTarget);
            Class<? extends BasicScript> scriptType = initialPassScriptTarget.getScriptClass();
            ScriptRunner<? extends BasicScript, ?> initialRunner = compiler.compile(scriptType, initialOperation, baseScope, Actions.doNothing());
            initialRunner.run(target, services);

            PluginRequests initialPluginRequests = getInitialPluginRequests(initialRunner);
            PluginRequests mergedPluginRequests = autoAppliedPluginHandler.mergeWithAutoAppliedPlugins(initialPluginRequests, target);

            PluginManagerInternal pluginManager = topLevelScript ? initialPassScriptTarget.getPluginManager() : null;
            pluginRequestApplicator.applyPlugins(mergedPluginRequests, scriptHandler, pluginManager, targetScope);

其中 getInitialPluginRequests 就是获得脚本里的 Plugin 请求,plugins { id 'com.android.application' },接着合并完所有的 Plugin 请求之后调用 applyPlugins 方法去解析 Plugin 请求。

    @Override
    public void applyPlugins(final PluginRequests requests, final ScriptHandlerInternal scriptHandler, @Nullable final PluginManagerInternal target, final ClassLoaderScope classLoaderScope) {
        if (target == null || requests.isEmpty()) {
            defineScriptHandlerClassScope(scriptHandler, classLoaderScope, Collections.emptyList());
            return;
        }

        final PluginResolver effectivePluginResolver = wrapInAlreadyInClasspathResolver(classLoaderScope);
        if (!requests.isEmpty()) {
            addPluginArtifactRepositories(scriptHandler.getRepositories());
        }
        List<Result> results = resolvePluginRequests(requests, effectivePluginResolver);

        // Could be different to ids in the requests as they may be unqualified
        final Map<Result, PluginId> legacyActualPluginIds = newLinkedHashMap();
        final Map<Result, PluginImplementation<?>> pluginImpls = newLinkedHashMap();
        final Map<Result, PluginImplementation<?>> pluginImplsFromOtherLoaders = newLinkedHashMap();

        if (!results.isEmpty()) {
            for (final Result result : results) {
                applyPlugin(result.request, result.found.getPluginId(), new Runnable() {
                    @Override
                    public void run() {
                        result.found.execute(new PluginResolveContext() {
                            @Override
                            public void addLegacy(PluginId pluginId, Object dependencyNotation) {
                                legacyActualPluginIds.put(result, pluginId);
                                scriptHandler.addScriptClassPathDependency(dependencyNotation);
                            }

                            @Override
                            public void add(PluginImplementation<?> plugin) {
                                pluginImpls.put(result, plugin);
                            }

                            @Override
                            public void addFromDifferentLoader(PluginImplementation<?> plugin) {
                                pluginImpls.put(result, plugin);
                                pluginImplsFromOtherLoaders.put(result, plugin);
                            }
                        });
                    }
                });
            }
        }

        defineScriptHandlerClassScope(scriptHandler, classLoaderScope, pluginImplsFromOtherLoaders.values());
        applyLegacyPlugins(target, legacyActualPluginIds);
        applyPlugins(target, pluginImpls);
    }

解释一下四个参数,第一个 PluginRequests 是通过读取 plugins{} 里的插件请求并且合并了自动添加的一些 core Plugin 的请求。第二个参数 ScriptHandlerInternal 的实现类是 DefaultScriptHandler,

public class DefaultScriptHandler implements ScriptHandler, ScriptHandlerInternal, DynamicObjectAware {
  ...
    @Override
    public void dependencies(Closure configureClosure) {
        ConfigureUtil.configure(configureClosure, getDependencies());
    }
  ...
}

其中的 dependencies 方法就是 buildscript {dependencies {}}的具体实现。第三个参数 PluginManagerInternal 的实现类,就是我在上文提到的 DefaultPluginManager,第四个参数 ClassLoaderScope Represents a particular node in the ClassLoader graph(这是源码里的注解,觉得的翻译的话影响理解)。

回归正题,applyPlugins 里第一步会通过 PluginResolver 去解析 PluginRequest,并且返回解析好的 Result,代码如下:

 private List<Result> resolvePluginRequests(PluginRequests requests, PluginResolver effectivePluginResolver) {
        return collect(requests, request -> {
            PluginRequestInternal configuredRequest = pluginResolutionStrategy.applyTo(request);
            return resolveToFoundResult(effectivePluginResolver, configuredRequest);
        });
    }

注意两点:1. PluginResolver 的实现有多个,它们串联在一起,将从头到尾搜索每一个 Resolver,直到找到插件。2. 每一个 PluginRequest 如果找到结果,就会封装成一个 Result 返回,在 Result 里有一个 PluginResolution。上一个代码片:

public class ClassPathPluginResolution implements PluginResolution {

   ...

    @Override
    public void execute(PluginResolveContext pluginResolveContext) {
        PluginRegistry pluginRegistry = new DefaultPluginRegistry(pluginInspector, parent);
        PluginImplementation<?> plugin = pluginRegistry.lookup(pluginId);
        if (plugin == null) {
            throw new UnknownPluginException("Plugin with id '" + pluginId + "' not found.");
        }
        pluginResolveContext.add(plugin);
    }
}

最终是通过 PluginImplementation<?> plugin = pluginRegistry.lookup(pluginId); DefaultPluginRegistry 来查找到 PluginImplementation。
如何找到 PluginImplementation,code 如下:

this.idMappings = CacheBuilder.newBuilder().build(new CacheLoader<PluginIdLookupCacheKey, Optional<PluginImplementation<?>>>() {
            @Override
            public Optional<PluginImplementation<?>> load(@Nonnull PluginIdLookupCacheKey key) {
                PluginId pluginId = key.getId();
                ClassLoader classLoader = key.getClassLoader();

                PluginDescriptorLocator locator = new ClassloaderBackedPluginDescriptorLocator(classLoader);

                PluginDescriptor pluginDescriptor = locator.findPluginDescriptor(pluginId.toString());
                if (pluginDescriptor == null) {
                    return Optional.empty();
                }

                String implClassName = pluginDescriptor.getImplementationClassName();
                if (!GUtil.isTrue(implClassName)) {
                    throw new InvalidPluginException(String.format("No implementation class specified for plugin '%s' in %s.", pluginId, pluginDescriptor));
                }

                final Class<?> implClass;
                try {
                    implClass = classLoader.loadClass(implClassName);
                } catch (ClassNotFoundException e) {
                    throw new InvalidPluginException(String.format(
                        "Could not find implementation class '%s' for plugin '%s' specified in %s.", implClassName, pluginId,
                        pluginDescriptor), e);
                }

                PotentialPlugin<?> potentialPlugin = pluginInspector.inspect(implClass);
                PluginImplementation<Object> withId = new RegistryAwarePluginImplementation(classLoader, pluginId, potentialPlugin);
                return Optional.of(withId);
            }
        });

首先这里的 classloader 就是之前所说的 ClassLoaderScope,接着通过这个 classloader 通过 PluginId 去查找 findPluginDescriptor:

    @Override
    public PluginDescriptor findPluginDescriptor(String pluginId) {
        URL resource = classLoader.getResource("META-INF/gradle-plugins/" + pluginId + ".properties");
        if (resource == null) {
            return null;
        } else {
            return new PluginDescriptor(resource);
        }
    }

写过 Gradle Plugin 的同学应该清楚,有一步需要在 META-INF/gradle-plugins/ 下声明 Plugin 的名字。作用就在这里体现了。implClass = classLoader.loadClass(implClassName); 就会把 Plugin load 到Gradle 的 runtime 中了,最终返回 Plugin 的实现。Plugin 的查找就告一段落,流程比较长,但比较有意思。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,386评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,142评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,704评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,702评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,716评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,573评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,314评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,230评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,680评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,873评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,991评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,706评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,329评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,910评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,038评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,158评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,941评论 2 355

推荐阅读更多精彩内容

  • Gradle 起底 第一篇 亦余心之所善兮,虽九死其犹未悔 Gradle 的源代码地址 https://githu...
    杰克熏阅读 904评论 4 10
  • Gradle 进阶 第四篇 天行健,君子以自强不息 微内核架构 前面的几篇文章,从 Gradle 脚本的函数调用一...
    杰克熏阅读 427评论 0 2
  • Gradle 入门 第二篇 精诚所至,金石为开。 Gradle 脚本的函数的调用 接着上一篇文章的尾巴,现在需要在...
    杰克熏阅读 745评论 2 7
  • Gradle 进阶 第三篇 破山中贼易,破心中贼难 Gradle Convention and Extension...
    杰克熏阅读 594评论 2 6
  • 久违的晴天,家长会。 家长大会开好到教室时,离放学已经没多少时间了。班主任说已经安排了三个家长分享经验。 放学铃声...
    飘雪儿5阅读 7,523评论 16 22