Gradle插件开发与执行原理浅析

这篇文章写的比较早,很多内容理解的不是很好,建议阅读本人最新文章Gradle开发快速入门——DSL语法原理与常用API介绍

环境

配置gradle(加入环境变量)

简单插件开发

新建一个gradle项目,在工程(主工程或子工程均可)根目录添加一个HelloPlugin.gradle文件

  1. apply plugin: HelloPlugin

  2. class MyExtension {

  3. Boolean enable = true

  4. String text = ''

  5. }

  6. class HelloPlugin implements Plugin<Project> {

  7. @Override

  8. void apply(Project project) {

  9. project.extensions.create('hello', MyExtension)

  10. project.task('hello') << {

  11. MyExtension ext = project.extensions.hello;

  12. if (ext.enable) {

  13. println "Hello ${ext.text}!"

  14. } else {

  15. println 'HelloPlugin is disabled.'

  16. }

  17. }

  18. }

  19. }

在工程根目录对应的build.gradle中添加以下代码

  1. // 调用HelloPlugin.gradle中的代码

  2. apply from: 'HelloPlugin.gradle'

  3. // 设置参数

  4. hello {

  5. enable = true

  6. text = 'World'

  7. }

在工程根目录执行命令行,即可看到插件中定义的Task(hello)被执行

  1. ➜ GradleStudy gradle hello

  2. :hello

  3. Hello World!

  4. BUILD SUCCESSFUL

  5. Total time: 0.675 secs

独立工程中开发插件

创建工程

后面的示例用命令行直接开发,先创建一个文件夹(project-dir)用于存放工程。用命令行写Java比较麻烦,但是之所以用命令行,是为了更好的理解gradle。

  1. ➜ ~ mkdir hello_proj
  2. ➜ ~ cd hello_proj
  3. ➜ hello_proj

也可以使用AndroidStudio或IDEA创建一个空的gradle项目。直接使用RootProject开发,则project-dir就是根目录;如果新建子模块开发,则project-dir就是这个模块的目录。

工程根目录下创建工程的gradle配置文件:<project-dir>/build.gradle

  1. ➜ hello_proj vim build.gradle

  2. apply plugin: 'groovy'

  3. apply plugin: 'maven'

  4. repositories {

  5. mavenCentral()

  6. }

  7. dependencies {

  8. // compile 'org.codehaus.groovy:groovy-all:2.3.11' // Groovy支持(远程)

  9. compile localGroovy() // Groovy支持(本地)

  10. compile gradleApi() // GradleAPI支持

  11. }

根目录再创建一个settings.build文件配置工程(如果直接使用RootProject,也可以不创建这个文件):<project-dir>/settings.gradle

  1. ➜ hello_proj vim settings.gradle

  2. rootProject.name = 'HelloPlugin'

创建源码

源码放在groovy插件默认的SourceSet源码目录下:<project-dir>/src/main/groovy/<package>/<class>.groovy

也可以在build.gradle中通过SourceSet命令指定源码和资源所在目录。

  1. ➜ hello_proj mkdir -pv src/main/groovy/com/jzj/groovy/

  2. ➜ hello_proj cd src/main/groovy/com/jzj/groovy

  3. ➜ groovy vim MyExtension.groovy

  4. ➜ groovy vim HelloPlugin.groovy

  5. package com.jzj.groovy

  6. class MyExtension {

  7. Boolean enable = true

  8. String text = ''

  9. }

  10. package com.jzj.groovy

  11. import org.gradle.api.Plugin

  12. import org.gradle.api.Project

  13. class HelloPlugin implements Plugin<Project> {

  14. @Override

  15. void apply(Project project) {

  16. project.extensions.create('hello', MyExtension)

  17. project.task('hello') << {

  18. MyExtension ext = project.extensions.hello;

  19. if (ext.enable) {

  20. println "Hello ${ext.text}!"

  21. } else {

  22. println 'HelloPlugin is disabled.'

  23. }

  24. }

  25. }

  26. }

创建资源文件

资源文件放在groovy插件默认的SourceSet资源目录下:<module-dir>/src/main/resources/META-INF/gradle-plugins/<plugin-name>.properties

示例中定义的plugin-name是HelloPlugin

  1. ➜ groovy cd ..

  2. ➜ jzj cd ..

  3. ➜ com cd ..

  4. ➜ groovy cd ..

  5. ➜ main mkdir -pv resources/META-INF/gradle-plugins

  6. ➜ main ls

  7. groovy resources

  8. ➜ main cd resources/META-INF/gradle-plugins

  9. ➜ gradle-plugins vim HelloPlugin.properties

  10. implementation-class=com.jzj.groovy.HelloPlugin

此时在命令行中查看文件结构如下。如果是IDEA或AndroidStudio,源码和资源文件的目录会被显示成对应的图标。

  1. ➜ hello_proj tree

  2. .

  3. ├── build.gradle

  4. ├── settings.gradle

  5. └── src

  6. └── main

  7. ├── groovy

  8. │ └── com

  9. │ └── jzj

  10. │ └── groovy

  11. │ ├── HelloPlugin.groovy

  12. │ └── MyExtension.groovy

  13. └── resources

  14. └── META-INF

  15. └── gradle-plugins

  16. └── HelloPlugin.properties

  17. 9 directories, 5 files

源码可在此下载

https://github.com/jzj1993/HelloGradlePlugin

打包

在命令行所在目录执行gradle build打包

  1. ➜ hello_proj gradle build

  2. :compileJava UP-TO-DATE

  3. :compileGroovy

  4. :processResources

  5. :classes

  6. :jar

  7. :assemble

  8. :compileTestJava UP-TO-DATE

  9. :compileTestGroovy UP-TO-DATE

  10. :processTestResources UP-TO-DATE

  11. :testClasses UP-TO-DATE

  12. :test UP-TO-DATE

  13. :check UP-TO-DATE

  14. :build

  15. BUILD SUCCESSFUL

  16. Total time: 1.714 secs

打包后目录结构如下,默认文件输出到build目录下,build/libs/HelloPlugin.jar就是最终生成的插件

  1. ➜ hello_proj tree

  2. .

  3. ├── build

  4. │ ├── classes

  5. │ │ └── main

  6. │ │ └── com

  7. │ │ └── jzj

  8. │ │ └── groovy

  9. │ │ ├── HelloPlugin$_apply_closure1.class

  10. │ │ ├── HelloPlugin.class

  11. │ │ └── MyExtension.class

  12. │ ├── libs

  13. │ │ └── HelloPlugin.jar

  14. │ ├── resources

  15. │ │ └── main

  16. │ │ └── META-INF

  17. │ │ └── gradle-plugins

  18. │ │ └── HelloPlugin.properties

  19. │ └── tmp

  20. │ ├── compileGroovy

  21. │ │ └── groovy-java-stubs

  22. │ └── jar

  23. │ └── MANIFEST.MF

  24. ├── build.gradle

  25. ├── settings.gradle

  26. └── src

  27. └── main

  28. ├── groovy

  29. │ └── com

  30. │ └── jzj

  31. │ └── groovy

  32. │ ├── HelloPlugin.groovy

  33. │ └── MyExtension.groovy

  34. └── resources

  35. └── META-INF

  36. └── gradle-plugins

  37. └── HelloPlugin.properties

  38. 24 directories, 11 files

插件使用

在需要使用插件的工程build.gradle中配置如下

BuildScript配置

通常在RootProject中的build.gradle中配置buildscript。和项目中的dependencies不同,buildscript代码块中的dependencies是在编译阶段需要依赖的包,而不会被编译进工程中。

在buildscript中添加对Gradle插件的依赖。

  • 对于已经发布到远程的插件,可以使用classpath 'group:name:version'的格式。
  • 也可以将插件发布到本地Maven仓库,在repositories中添加本地maven仓库。
  • 还可以直接用本地jar文件,使用classpath files('xxx.jar')的方式依赖。
  1. buildscript {
  2. repositories {
  3. jcenter()
  4. // maven { url uri('../repo') } // 指定本地maven仓库的路径
  5. // ...
  6. }
  7. dependencies {
  8. // classpath 'com.jzj.gradle:HelloPlugin:0.0.1' // 依赖远程插件
  9. classpath files('HelloPlugin.jar') // 依赖本地文件,需要将jar文件放到项目根目录
  10. }
  11. }

Project配置

对于需要使用插件的Project,在其build.gradle中添加下面的脚本。

  1. // 应用插件

  2. apply plugin: 'HelloPlugin'

  3. // 插件配置

  4. hello {

  5. enable = true

  6. text = 'World'

  7. }

执行

在Project根目录运行gradle指令即可看到效果。

  1. ➜ GradleStudy gradle hello

  2. :hello

  3. Hello World!

  4. BUILD SUCCESSFUL

  5. Total time: 0.675 secs

执行原理浅析

Groovy

  1. Groovy是一种脚本语言,在Java基础上进行了一些扩展,支持闭包、动态类型等特性,兼容Java代码。
  2. 每个Groovy脚本文件会编译生成一个继承自groovy.lang.Script的Java class。

动态类型

  1. def var = 'text'

  2. println var

  3. var = 5

  4. println var + 1

  5. Object var = "text";

  6. System.out.println((String)o);

  7. var = 5;

  8. System.out.println(String.valueof((Integer)o + 1));

闭包 Closure

  1. Closure c = { a, b ->

  2. println a

  3. println b

  4. }

  5. c.call('text1', 5)

  6. public static void main(String[] args) {

  7. abstract class MyClosure {

  8. abstract void call(Object a, Object b);

  9. }

  10. MyClosure c = new MyClosure() {

  11. @Override

  12. void call(Object a, Object b) {

  13. System.out.println(a);

  14. System.out.println(b);

  15. }

  16. };

  17. c.call("text1", 5);

  18. }

代理对象 DelegateObject

  • 每个闭包都有一个代理对象,在闭包上未找到的属性和方法都会转给代理对象。
  1. class MyDelegate {

  2. def func() {

  3. println 'func'

  4. }

  5. }

  6. Closure c = {

  7. func();

  8. }

  9. c.delegate = new MyDelegate()

  10. c.call()

  11. public static void main(String[] args) {

  12. class MyDelegate {

  13. void func() {

  14. System.out.println("func");

  15. }

  16. }

  17. abstract class MyClosure {

  18. Object delegate;

  19. boolean callMethod(Object o, String method, Object... args) {

  20. try {

  21. Method func = o.getClass().getDeclaredMethod(method);

  22. if (func != null) {

  23. func.invoke(o, args);

  24. return true;

  25. }

  26. } catch (Exception e) {

  27. // do nothing.

  28. }

  29. return false;

  30. }

  31. abstract void call();

  32. }

  33. MyClosure c = new MyClosure() {

  34. @Override

  35. void call() {

  36. if (!callMethod(this, "func")) {

  37. callMethod(delegate, "func");

  38. }

  39. }

  40. };

  41. c.delegate = new MyDelegate();

  42. c.call();

  43. }

this, owner, delegate

  1. class Cls {

  2. def mCls = this;

  3. def a, b, c;

  4. def static assertSameObj(x, y) {

  5. assert x.is(y);

  6. }

  7. def static assertSameObj(Closure x, y) {

  8. assert x.is(y);

  9. }

  10. def start() {

  11. println 'this = ' + this

  12. a = {

  13. println 'a.this = ' + this

  14. println 'a.owner = ' + owner

  15. println 'a.delegate = ' + delegate

  16. assertSameObj(this, mCls)

  17. assertSameObj(owner, mCls)

  18. assertSameObj(delegate, owner)

  19. b = {

  20. println 'b.this = ' + this

  21. println 'b.owner = ' + owner

  22. println 'b.delegate = ' + delegate

  23. assertSameObj(this, mCls)

  24. assertSameObj(owner, a)

  25. assertSameObj(delegate, owner)

  26. c = {

  27. println 'c.this = ' + this

  28. println 'c.owner = ' + owner

  29. println 'c.delegate = ' + delegate

  30. assertSameObj(this, mCls)

  31. assertSameObj(owner, b)

  32. assertSameObj(delegate, owner)

  33. }

  34. println('c = ' + c)

  35. c.call();

  36. // 修改c的delegate,并调用c没有但delegate有的方法

  37. c.delegate = new String('123');

  38. println('length = ' + c.length())

  39. }

  40. println('b = ' + b)

  41. b.call();

  42. }

  43. println('a = ' + a)

  44. a.call()

  45. }

  46. }

  47. new Cls().start();

  48. this = [email protected]

  49. a = [email protected]

  50. a.this = [email protected]

  51. a.owner = [email protected]

  52. a.delegate = [email protected]

  53. b = [email protected]

  54. b.this = [email protected]

  55. b.owner = [email protected]

  56. b.delegate = [email protected]

  57. c = [email protected]

  58. c.this = [email protected]

  59. c.owner = [email protected]

  60. c.delegate = [email protected]

  61. length = 3

结论:

  • 语法上直接将Closure赋值给Object编译器会有警告,但不影响实际运行。闭包最终也是通过Object实现的。

  • this指向其外部的Object对象,指定义闭包的类

  • owner指向其外部的Object/Closure,指直接包含闭包的类或闭包

  • delegate默认和owner一致,且可以修改,指用于解析闭包中属性和方法调用的第三方对象

Gradle

  1. Gradle为基于Groovy的一种领域专用语言(DSL/Domain Specific Language)

  2. 每个Gradle脚本文件编译生成的类除了继承自groovy.lang.Script,同时还实现了接口org.gradle.api.Script

  3. Gradle工程build时,会执行settings.gradlebuild.gradle脚本;settings脚本的代理对象是Setting对象,build脚本的代理对象是Project对象。

Gradle Delegate

Build脚本对应的Project对象从6个范围中查找方法:

  1. Project对象本身定义的方法
  2. 脚本文件中定义的方法
  3. 被插件添加的extension. extension的名字可以做为方法名
  4. 被插件添加的convension方法。
  5. 工程中的task。task的名字可以作为方法名
  6. 父工程中的方法。

例如在build.gradle中,常会使用dependencies语句块。

  1. // build.gradle

  2. dependencies {

  3. compile 'xxx:xxx:1.0'

  4. testCompile 'xxx:xxx:1.0'

  5. }

  • dependencies是Project对象中定义的DSL方法,后面的大括号是其接受的闭包参数;这里Groovy的括号可以省略;

  • 闭包的delegate是DependencyHandler getDependencies(),因此其内部可以直接调用DependencyHandler定义的compile/testCompile等方法;

下面的写法也是可以的。

  1. // build.gradle

  2. dependencies {

  3. compile 'xxx'

  4. }

  5. getProject().dependencies ({

  6. compile('xxx')

  7. })

  8. getProject().getDependencies().compile('xxx')

  9. project.dependencies.compile('xxx')

apply plugin

如果分析gradle的源码可以知道,执行apply plugin时,会执行Plugin的apply方法,apply中project.extensions.create('hello', MyExtension)动态给Project对象创建了名为hello的extensions,因此在apply之后,可以使用hello(Closure)

如果把apply和MyExtention的位置调换,gradle编译时就会报错,提示找不到DSL。

  1. Error:(4, 0) Gradle DSL method not found: 'hello()'
  2. Possible causes:<ul><li>The project 'Gradle' may be using a version of Gradle that does not contain the method.
  3. <a href="open.wrapper.file">Open Gradle wrapper file</a></li><li>The build file may be missing a Gradle plugin.
  4. <a href="apply.gradle.plugin">Apply Gradle plugin</a></li>

Internal Gradle Plugins

  • 从Gradle源码可以看到其内部实现了JavaPluginGroovyPluginWarPlugin(JavaWebApplication)等基础插件。

  • 实际上dependencies中的compile就是由JavaBasePlugin动态创建的一个Configuration DSL。

  • groovy、android等插件在apply时也会先apply JavaBasePlugin,所以就有了compile、sourceSet等DSL。

  1. // org.gradle.api.plugins.JavaBasePlugin.java

  2. private void defineConfigurationsForSourceSet(SourceSet sourceSet, ConfigurationContainer configurations) {

  3. Configuration compileConfiguration = configurations.maybeCreate(sourceSet.getCompileConfigurationName());

  4. compileConfiguration.setVisible(false);

  5. compileConfiguration.setDescription(String.format("Dependencies for %s.", sourceSet));

  6. // ...

  7. sourceSet.setCompileClasspath(compileClasspathConfiguration);

  8. sourceSet.setRuntimeClasspath(sourceSet.getOutput().plus(runtimeConfiguration));

  9. }

  10. // groovy plugin

  11. public void apply(ProjectInternal project) {

  12. project.getPluginManager().apply(JavaBasePlugin.class);

  13. // ...

  14. }

build流程

Gradle脚本的build流程分为3个阶段:

  1. 初始化阶段
    执行Settings脚本。Gradle支持单个和多个工程的编译。在初始化阶段,Gradle判断需要参与编译的工程,为每个工程创建一个Project对象,并建立工程之间的层次关系。

  2. 配置阶段
    执行Build脚本。Gradle对上一步创建的Project对象进行配置。

  3. 执行阶段
    执行选中的task,例如build,assembleDebug等。

参考资料与扩展阅读

  1. 《Gradle脚本基础全攻略》 http://blog.csdn.net/yanbober/article/details/49314255

  2. 《Groovy脚本基础全攻略》 http://blog.csdn.net/yanbober/article/details/49047515

  3. 《Chapter 40. Writing Custom Plugins》 https://docs.gradle.org/current/userguide/custom_plugins.html

  4. 《GRADLE脚本的语法和BUILD流程》 http://www.jianshu.com/p/20f6695a9bd5

  5. 《深入理解Android(一):Gradle详解》 http://www.infoq.com/cn/articles/android-in-depth-gradle/