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