Gradle系列6--内置插件

作者: cntlb | 来源:发表于2018-11-22 16:18 被阅读1次

    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的方式可以应用在构建的任何一个地方。

    总结

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

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

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

    相关文章

      网友评论

        本文标题:Gradle系列6--内置插件

        本文链接:https://www.haomeiwen.com/subject/feilqqtx.html