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必須在構建腳本開頭位置。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章