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必须在构建脚本开头位置。

    原文作者:cntlb
    原文地址: https://www.jianshu.com/p/8fa851716020
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞