Gradle系列6--内置插件

Gradle系列基础上,本文以apply plugin:'java'为例介绍Gradle内置的插件及其应用原理解析.

简单的Gradle工程开始

先创建一个Gradle项目(项目叫plugin-analysis, 模块hello为java-application):

 $ mkdir plugin-analysis && cd plugin-analysis
 $ gradle wrapper --gradle-version=4.6 --distribution-type=all
 $ ./gradlew init #创建setting.gradle和只有注释的build.gradle
 $ mkdir hello && cd hello/
 $ ../gradlew init --type=java-application
 $ rm -r gradle* sett* && cd -
 $ echo -e "\rinclude ':hello'" >> settings.gradle
 $ ./gradle build

精简后的hello/build.gradle如下

plugins {
    id 'java'
    id 'application'
}

mainClassName = 'App'

dependencies {
    compile 'com.google.guava:guava:23.0'
    testCompile 'junit:junit:4.12'
}

repositories {
    jcenter()
}

其中plugins {id 'java'}也可以写成apply plugin:'java', 都是应用插件。通过自定义Gradle插件可以猜测java插件不是从buildSrc中来的(项目中根本没有该目录),可以多个项目共享的插件应该是某个独立的项目的成品——jar包。java插件如果从jar包中来的,回顾自定义插件,这个jar包中一定有一个META-INF/gradle.plugins/java.properties文件(插件id和properties文件映射关系)。顶级build.gradle这时还没有添加任何额外的配置,里面都只是注释,于是这个jar包是不是的路径是不是有可能是内置的?来找找这个jar包的老巢。

遥想当年初学java的时候配置的环境变量中就有一个CLASSPATH=$JAVA_HOME/lib/dt.jar类似的配置,现在到GRADLE_HOME/lib会发现里面有一个plugins目录,找一些比较能和java插件相关的jar包看看

plugins $ ls|grep -E "gradle|java"
...
gradle-ide-4.6.jar
gradle-ide-native-4.6.jar
gradle-ide-play-4.6.jar
...
gradle-language-groovy-4.6.jar
gradle-language-java-4.6.jar
gradle-language-jvm-4.6.jar
gradle-language-native-4.6.jar
gradle-language-scala-4.6.jar
...
gradle-platform-base-4.6.jar
gradle-platform-jvm-4.6.jar
gradle-platform-native-4.6.jar
gradle-platform-play-4.6.jar
gradle-plugin-development-4.6.jar
gradle-plugins-4.6.jar

采用"jar肉搜索"技术,打开每个jar包直奔META-INF/gradle.plugins目录看下有没有一个叫做java.properties文件——并没有。gradle-plugins-4.6.jar:META-INF/gradle.plugins如下:

org.gradle.application.properties
org.gradle.base.properties
org.gradle.distribution.properties
org.gradle.groovy-base.properties
org.gradle.groovy.properties
org.gradle.java-base.properties
org.gradle.java-library-distribution.properties
org.gradle.java-library.properties
org.gradle.java.properties
org.gradle.war.properties

但这些文件有一个公共的前缀org.gradle.,有没有试过用我们的方式去应用这些插件,比如org.gradle.java。为了测试,建议使用集成开发环境,这里采用IntelliJ IDEA打开刚创建的plugin-analysis然后可以直接运行App.main方法(或执行./gradlew :hello:run)。现在将hello/build.gradle中的插件都加上前缀:

plugins {
    id 'org.gradle.java'
    id 'org.gradle.application'
}

依然可以运行!为什么可以这样?
考虑如果让你来设计gradle构建工具, 用户喜欢长id还是短id? 插件id设计的目的是为了插件容易记住, 自然越短的id越容易记了。

apply plugin:'java'

插件可以用于模块化和重用工程配置,当build.gradle中的apply plugin:'java'执行时调用PluginAware.apply(java.util.Map) (Project 实现PluginAware, 再次强烈建议使用集成环境来查看源码)

org.gradle.api.internal.project.AbstractPluginAware

public void apply(Map<String, ?> options) {
  DefaultObjectConfigurationAction action = createObjectConfigurationAction();
  ConfigureUtil.configureByMap(options, action);
  action.execute();
}

org.gradle.api.internal.plugins.DefaultObjectConfigurationAction

public void execute() {
  //apply plugin:'java'时到这里targets是空的
  if (targets.isEmpty()) {
    to(defaultTarget);
  }

  //actions在plugin()方法中添加元素, 当前讨论的方式对应plugin(String)
  for (Runnable action : actions) {
    action.run();
  }
}

public ObjectConfigurationAction plugin(final String pluginId) {
  actions.add(new Runnable() {//execute()中遍历执行run()
    public void run() {
      applyType(pluginId); 
    }
  });
  return this;
}

// 应用插件
private void applyType(String pluginId) {
  for (Object target : targets) {
    if (target instanceof PluginAware) {
      ((PluginAware) target).getPluginManager().apply(pluginId);
    } else {
      throw new XxxException(...);
    }
  }
}

上面getPluginManager()返回DefaultPluginManager对象

// org.gradle.api.internal.plugins.DefaultPluginManager类
private final PluginRegistry pluginRegistry;
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);
}

lookup()的实现在DefaultPluginRegistry

//org.gradle.api.internal.plugins.DefaultPluginRegistry#lookup(org.gradle.plugin.use.PluginId)
public PluginImplementation<?> lookup(PluginId pluginId) {
  //...保留当前讨论有用的代码
  return lookup(pluginId, classLoaderScope.getLocalClassLoader());
}

@Nullable
private PluginImplementation<?> lookup(PluginId pluginId, ClassLoader classLoader) {
  // Don't go up the parent chain.
  // Don't want to risk classes crossing “scope” boundaries and being non collectible.

  PluginImplementation lookup;
  if (pluginId.getNamespace() == null) {
    //DefaultPluginManager.CORE_PLUGIN_NAMESPACE = "org.gradle"
    PluginId qualified = pluginId.withNamespace(DefaultPluginManager.CORE_PLUGIN_NAMESPACE);
    //后面代码可以不用管了,自行研究。知道这里产生了完整的插件id即可
    lookup = uncheckedGet(idMappings, new PluginIdLookupCacheKey(qualified, classLoader)).orNull();
    if (lookup != null) {
      return lookup;
    }
  }

  return uncheckedGet(idMappings, new PluginIdLookupCacheKey(pluginId, classLoader)).orNull();
}

org.gradle.plugin.use.internal.DefaultPluginId

public class DefaultPluginId implements PluginId {
  public static final String SEPARATOR = ".";
  private final String value;
  public DefaultPluginId(String value) {
    this.value = value;
  }
  public static PluginId unvalidated(String value) {
    return new DefaultPluginId(value);
  }
  
  private boolean isQualified() {
    return value.contains(SEPARATOR);
  }

  @Override
  public PluginId withNamespace(String namespace) {
    if (isQualified()) {
      throw new IllegalArgumentException(this + " is already qualified");
    } else {
      return new DefaultPluginId(namespace + SEPARATOR + value);
    }
  }

  public String getNamespace() {
    return isQualified() ? value.substring(0, value.lastIndexOf(SEPARATOR)) : null;
  }

  @Override
  public String getName() {
    return isQualified() ? value.substring(value.lastIndexOf(SEPARATOR) + 1) : value;
  }

  @Override
  public String getId() {
    return value;
  }
}

上面贴出来的关键代码中可以看出apply plugin:'java'会创建一个PluginId对象,它的getNamespace()根据传入的字符串(这里是"java")中如果没有SEPARATOR(".")就返回null, 于是通过pluginId.withNamespace("org.gradle")创建一个完整的id(org.gradle.java)。

至此已经完全明白了从apply plugin:'java'apply plugin:'org.gradle.java'的转换:

  • 如果id中含有"."作为分隔符,那么就不需要转换,此时必须能确切的找到一个"$id.properties"文件,否则抛出异常new UnknownPluginException("Plugin with id '" + pluginId + "' not found.")
  • 如果id中没有"."作为分隔符,自动加上"org.gradle."前缀来生成完整的id

plugins {id 'java'}呢?

Plugins

Plugins can be used to modularise and reuse project configuration. Plugins can be applied using the PluginAware.apply(java.util.Map) method, or by using the PluginDependenciesSpec plugins script block.

plugins {id 'java'}的方式使用的就是在plugins块中定义 PluginDependenciesSpec ,来看一下PluginAware#getPlugins的实现

// AbstractPluginAware
public PluginContainer getPlugins() {
  // getPluginManager前面有. 方法返回DefaultPluginContainer
  return getPluginManager().getPluginContainer();
}

//DefaultPluginContainer
public Plugin apply(String id) {
  // 重复上面apply的故事了
  PluginImplementation plugin = pluginRegistry.lookup(DefaultPluginId.unvalidated(id));
  if (plugin == null) {
    throw new UnknownPluginException("Plugin with id '" + id + "' not found.");
  }
  
  if (!Plugin.class.isAssignableFrom(plugin.asClass())) {
     // plugin不是Plugin就抛异常
     throw new IllegalArgumentException("Plugin implementation '" + plugin.asClass().getName() + "' does not implement the Plugin interface. This plugin cannot be applied directly via the PluginContainer.");
  } else {
    return pluginManager.addImperativePlugin(plugin);
  }
}

特别需要说明的是上面的PluginImplementation必须是我们自定义Gradle插件中所熟悉的Plugin的实现类!

由于plugins{}是配置一个PluginContainer对象,并没有直接调用相关的apply()方法,因此通过plugins方式应用的插件必须放在build.gradle的开头,读取配置自动应用插件。而apply的方式可以应用在构建的任何一个地方。

牛刀小试

开发Android对google提供的gradle插件一定不会陌生。对于'com.android.tools.build:gradle:3.2.1'($groupId:$artifactId:$version)来说,它位于google()仓库,android-studio3.2.1自带的,仓库位于android-studio/gradle/m2repository/。结合自定义插件,仓库下com/android/tools/build/gradle/gradle-3.2.1.jar("$groupId/$artifactId/$version".replace('.','/'))。

我们来看META-INF/gradle-plugins

android-library.properties
android.properties
android-reporting.properties
com.android.application.properties
com.android.base.properties
com.android.debug.structure.properties
com.android.dynamic-feature.properties
com.android.feature.properties
com.android.instantapp.properties
com.android.library.properties
com.android.lint.properties
com.android.test.properties

只要把后缀.properties去掉就得到了插件的id。android.propertiescom.android.application.properties的一个别名,里面是完全一样的实现类。有意思的是apply plugin:'android'已被弃用但还可以使用,为什么这里的android插件id中不含"."可以加载出来呢?这就要考虑到Gradle构建工具设计对外都是面向接口的,允许PluginAware#getPluginManager实现不同的插件加载策略。

至此面对apply plugin: 'com.android.application'等等已不再陌生,对于一个新的Gradle插件我们也知道如何去使用以及查看源代码。

总结

Gradle构建工具默认情况下,对于插件的应用关键是找到Plugin的实现类执行其apply方法,如果是一个具体的类就已经找到了。对于id的方式使用插件

  • 如果id中含有 "." 作为分隔符,那么就不需要转换,此时必须能确切的找到一个"$id.properties"文件,否则抛出异常UnknownPluginException("Plugin with id '" + pluginId + "' not found.")
  • 如果id中没有 "." 作为分隔符,自动加上"org.gradle."前缀来生成完整的id

最后apply可以在构建脚本的任何地方使用,而plugins必须在构建脚本开头位置。

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

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,657评论 18 139
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,822评论 6 342
  • 目录 实践使用插件开发插件编写插件优先编写和使用自定义任务类型受益于增量任务建模类DSL的API捕获用户输入来配置...
    spt_genius阅读 1,608评论 0 4
  • 探讨Gradle插件的自定义有助更好的理解Gradle,本文综合了userguide中的几篇相关文档,将不单独给出...
    cntlb阅读 2,494评论 0 7
  • 说明 本文主要从实现原理和代码层面介绍Gradle开发相关知识。关于本文中提到的、Gradle中的基本概念等内容,...
    jzj1993阅读 7,923评论 1 33