(快速参考)

17 插件

版本 6.2.0

17 插件

Grails 首先是一个 Web 应用程序框架,但它也是一个平台。通过公开许多扩展点,您可以扩展从命令行界面到运行时配置引擎的任何内容,Grails 可以定制以满足几乎任何需求。要连接到此平台,您需要做的就是创建一个插件。

扩展平台听起来很复杂,但插件的范围可以从非常简单到非常强大。如果您知道如何构建 Grails 应用程序,您就会知道如何创建插件来共享数据模型或一些静态资源。

17.1 创建和安装插件

创建插件

创建 Grails 插件只需运行以下命令:

grails create-plugin <<PLUGIN NAME>>

这将为您指定的名称创建一个 Web 插件项目。例如,运行 grails create-plugin example 将创建一个名为 example 的新 Web 插件项目。

在 Grails 3.0 中,您应该考虑创建的插件是否需要 Web 环境,或者该插件是否可以与其他配置文件一起使用。如果您的插件不需要 Web 环境,则使用 “plugin” 配置文件而不是默认的 “web-plugin” 配置文件。

grails create-plugin <<PLUGIN NAME>> --profile=plugin

确保插件名称中没有连续出现多个大写字母,否则将无法使用。不过,驼峰式命名是可以的。

作为常规 Grails 项目有很多好处,您可以通过运行(如果插件的目标是 “web” 配置文件)立即测试您的插件:

./gradlew bootRun
插件项目默认不提供 index.gsp,因为大多数插件不需要它。因此,如果您尝试在创建插件后立即在浏览器中查看它,您将收到页面未找到错误。如果您愿意,可以轻松地为您的插件创建一个 grails-app/views/index.gsp

Grails 插件的结构与 Grails 应用程序项目的结构几乎相同,不同之处在于,在插件包结构下的 src/main/groovy 目录中,您将找到一个插件描述符类(以 “GrailsPlugin” 结尾的类)。例如:

import grails.plugins.*

class ExampleGrailsPlugin extends Plugin {
   ...
}

所有插件都必须在 src/main/groovy 目录下有这个类,否则它们不被视为插件。插件类定义了有关插件的元数据,以及可选地连接到插件扩展点(稍后介绍)。

您还可以使用几个特殊属性提供有关插件的其他信息:

  • title - 插件的一句话简短描述

  • grailsVersion - 插件支持的 Grails 版本范围。例如 “1.2 > *”(表示 1.2 或更高版本)

  • author - 插件作者的姓名

  • authorEmail - 插件作者的联系电子邮件

  • developers - 除上述作者之外的任何其他开发人员。

  • description - 插件功能的完整多行描述

  • documentation - 插件文档的 URL

  • license - 插件的许可证

  • issueManagement - 插件的问题跟踪器

  • scm - 插件的源代码管理位置

以下是Quartz Grails 插件的一个简化示例:

package quartz

@Slf4j
class QuartzGrailsPlugin extends Plugin {
    // the version or versions of Grails the plugin is designed for
    def grailsVersion = "3.0.0.BUILD-SNAPSHOT > *"
    // resources that are excluded from plugin packaging
    def pluginExcludes = [
            "grails-app/views/error.gsp"
    ]
    def title = "Quartz" // Headline display name of the plugin
    def author = "Jeff Brown"
    def authorEmail = "[email protected]"
    def description = '''\
Adds Quartz job scheduling features
'''
    def profiles = ['web']
    List loadAfter = ['hibernate3', 'hibernate4', 'hibernate5', 'services']
    def documentation = "https://grails.groovy-lang.cn/plugin/quartz"
    def license = "APACHE"
    def issueManagement = [ system: "Github Issues", url: "http://github.com/grails3-plugins/quartz/issues" ]
    def developers = [
            [ name: "Joe Dev", email: "[email protected]" ]
    ]
    def scm = [ url: "https://github.com/grails3-plugins/quartz/" ]

    Closure doWithSpring()......

插件配置

不要直接访问 Grails 配置,例如 grailsApplication.config.getProperty('mail.hostName', String),而是使用使用 ConfigurationProperties 注释注释的 Spring Boot 配置 bean(或 POJO)。以下是一个插件配置示例:

./src/main/groovy/example/MailPluginConfiguration.groovy

package example

import org.springframework.boot.context.properties.ConfigurationProperties

@ConfigurationProperties(prefix = "mail")
class MailPluginConfiguration {

    String hostName
    int port
    String from
}

您可以像任何其他 bean 一样将 MailPluginConfiguration bean 注入到您的 bean 中。

./grails-app/services/example/MailService.groovy

package example

class MailService {

    MailPluginConfiguration mailPluginConfiguration

    void sendMail() {

    }

}

请阅读Spring Boot 外部化配置部分以获取更多信息。

安装本地插件

为了将 Grails 插件安装到本地 Maven,您可以使用 Gradle Maven Publish 插件。您可能还需要将发布扩展配置为:

publishing {
    publications {
        maven(MavenPublication) {
            versionMapping {
                usage('java-api') {
                    fromResolutionOf('runtimeClasspath')
                }
                usage('java-runtime') {
                    fromResolutionResult()
                }
            }
            from components.java
        }
    }
}
有关最新信息,请参阅 Gradle Maven Publish 插件文档。

要使您的插件可用于 Grails 应用程序,请运行 ./gradlew publishToMavenLocal 命令。

./gradlew publishToMavenLocal

这会将插件安装到您的本地 Maven 缓存中。然后,要在应用程序中使用该插件,请在 build.gradle 文件中声明对该插件的依赖关系,并在您的仓库哈希中包含 mavenLocal()

...
repositories {
    ...
    mavenLocal()
}
...
implementation "org.grails.plugins:quartz:0.1"
在 Grails 2.x 中,插件被打包为 ZIP 文件,但在 Grails 3.x 中,插件是简单的 JAR 文件,可以添加到 IDE 的类路径中。

插件和多项目构建

如果您希望将插件设置为多项目构建的一部分,请按照以下步骤操作。

步骤 1:创建应用程序和插件

使用 grails 命令创建一个应用程序和一个插件:

$ grails create-app myapp
$ grails create-plugin myplugin

步骤 2:创建 settings.gradle 文件

在同一目录中创建一个 settings.gradle 文件,内容如下:

include "myapp", "myplugin"

目录结构应如下所示:

PROJECT_DIR
  - settings.gradle
  - myapp
    - build.gradle
  - myplugin
    - build.gradle

步骤 3:声明对插件的项目依赖关系

在应用程序的 build.gradle 中,在 plugins 块中声明对插件的依赖关系:

grails {
    plugins {
        implementation project(':myplugin')
    }
}
您也可以在 dependencies 块中声明依赖关系,但是如果您这样做,您将不会获得子项目重新加载!

步骤 4:配置插件以启用重新加载

在插件目录中,添加或修改 gradle.properties 文件。需要设置一个新属性 exploded=true,以便插件将展开的目录添加到类路径中。

步骤 5:运行应用程序

现在,从应用程序目录的根目录使用 ./gradlew bootRun 命令运行应用程序,您可以使用 verbose 标志查看 Gradle 输出:

$ cd myapp
$ ./gradlew bootRun --verbose

您会从 Gradle 输出中注意到,插件源代码已构建并放置在应用程序的类路径中。

:myplugin:compileAstJava UP-TO-DATE
:myplugin:compileAstGroovy UP-TO-DATE
:myplugin:processAstResources UP-TO-DATE
:myplugin:astClasses UP-TO-DATE
:myplugin:compileJava UP-TO-DATE
:myplugin:configScript UP-TO-DATE
:myplugin:compileGroovy
:myplugin:copyAssets UP-TO-DATE
:myplugin:copyCommands UP-TO-DATE
:myplugin:copyTemplates UP-TO-DATE
:myplugin:processResources
:myapp:compileJava UP-TO-DATE
:myapp:compileGroovy
:myapp:processResources UP-TO-DATE
:myapp:classes
:myapp:findMainClass
:myapp:bootRun
Grails application running at http://localhost:8080 in environment: development

关于排除工件的说明

尽管 create-plugin 命令为您创建了某些文件,以便可以将插件作为 Grails 应用程序运行,但并非所有这些文件都包含在打包插件中。以下是创建但未包含在 package-plugin 中的工件列表:

  • grails-app/build.gradle(尽管它用于生成 dependencies.groovy

  • grails-app/conf/application.yml(重命名为 plugin.yml)

  • grails-app/conf/spring/resources.groovy

  • grails-app/conf/logback.groovy

  • /src/test/*\* 中的所有内容

  • *\*/.svn/*\**\*/CVS/*\* 中的 SCM 管理文件

自定义插件内容

在开发插件时,您可能会创建在插件的开发和测试期间使用的测试类和源代码,但不应将其导出到应用程序中。

要排除测试源,您需要修改插件描述符的 pluginExcludes 属性,并在 build.gradle 文件中排除资源。例如,假设您在插件源代码树中有一些 com.demo 包下的类,但不应将其打包在应用程序中。在您的插件描述符中,您应该排除这些类:

// resources that should be loaded by the plugin once installed in the application
  def pluginExcludes = [
    '**/com/demo/**'
  ]

在您的 build.gradle 中,您应该从 JAR 文件中排除编译后的类:

jar {
  exclude "com/demo/**/**"
}

Grails 3.0 中的内联插件

在 Grails 2.x 中,可以在 BuildConfig 中指定内联插件,在 Grails 3.x 中,此功能已被 Gradle 的多项目构建功能取代。

要设置多项目构建,请在父目录中创建一个应用程序和一个插件:

$ grails create-app myapp
$ grails create-plugin myplugin

然后在父目录中创建一个 settings.gradle 文件,指定应用程序和插件的位置:

include 'myapp', 'myplugin'

最后,在应用程序的 build.gradle 中添加对插件的依赖关系:

implementation project(':myplugin')

使用此技术,您已经实现了 Grails 2.x 中内联插件的等效功能。

17.2 插件仓库

在 Grails Central 插件仓库中分发插件

分发插件的首选方法是发布到官方的 Grails Central 插件仓库。这将使您的插件对 list-plugins 命令可见:

grails list-plugins

该命令列出了中央仓库中的所有插件。您的插件也可以使用 plugin-info 命令:

grails plugin-info [plugin-name]

该命令打印有关它的额外信息,例如它的描述、作者等。

如果您已经创建了一个 Grails 插件并希望将其托管在中央仓库中,您可以在 插件门户网站上找到有关获取帐户的说明。

17.3 提供基本工件

添加命令行命令

插件可以通过以下两种方式之一将新命令添加到 Grails 3.0 交互式 shell 中。首先,使用 create-script 命令,您可以创建一个代码生成脚本,该脚本将可用于应用程序。create-script 命令将在 src/main/scripts 目录中创建脚本:

+ src/main/scripts     <-- additional scripts here
 + grails-app
      + controllers
      + services
      + etc.

代码生成脚本可用于在项目树中创建工件并自动执行与 Gradle 的交互。

如果要创建一个与加载的 Grails 应用程序实例交互的新 shell 命令,则应使用 create-command 命令:

$ grails create-command MyExampleCommand

这将创建一个名为 grails-app/commands/PACKAGE_PATH/MyExampleCommand.groovy 的文件,该文件扩展了 ApplicationCommand

import grails.dev.commands.*

class MyExampleCommand implements ApplicationCommand {

  boolean handle(ExecutionContext ctx) {
      println "Hello World"
      return true
  }
}

ApplicationCommand 可以访问 GrailsApplication 实例,并且像任何其他 Spring bean 一样受自动装配的约束。

您还可以在命令中使用一个简单属性来通知 Grails 跳过 Bootstrap.groovy 文件的执行:

class MyExampleCommand implements ApplicationCommand {

  boolean skipBootstrap = true

  boolean handle(ExecutionContext ctx) {
      ...
  }
}

对于存在的每个 ApplicationCommand,Grails 将创建一个 shell 命令和一个 Gradle 任务来调用 ApplicationCommand。在上面的示例中,您可以使用以下任一方法调用 MyExampleCommand 类:

$ grails my-example

或者

$ gradle myExample

Grails 版本全部为小写字母,并用连字符分隔,不包括 “Command” 后缀。

代码生成脚本和 ApplicationCommand 实例之间的主要区别在于,后者可以完全访问 Grails 应用程序状态,因此可以用来执行与数据库交互、调用 GORM 等任务。

在 Grails 2.x 中,Gant 脚本可以用来执行这两项任务,在 Grails 3.x 中,代码生成和与运行时应用程序状态的交互已经被清晰地分离。

添加新的 grails-app 工件(控制器、标签库、服务等)

插件可以通过在 grails-app 树中创建相关文件来添加新的工件。

+ grails-app
      + controllers  <-- additional controllers here
      + services <-- additional services here
      + etc.  <-- additional XXX here

提供视图、模板和视图解析

当一个插件提供一个控制器时,它也可以提供默认的视图来渲染。这是一个通过插件模块化应用程序的极好方法。Grails 的视图解析机制将首先在安装应用程序的应用程序中查找视图,如果失败,将尝试在插件中查找视图。这意味着您可以通过在应用程序的 grails-app/views 目录中创建相应的 GSP 来覆盖插件提供的视图。

例如,考虑一个名为 BookController 的控制器,它是由一个名为 'amazon' 的插件提供的。如果正在执行的操作是 list,Grails 将首先查找一个名为 grails-app/views/book/list.gsp 的视图,如果失败,它将在插件的相对路径中查找相同的视图。

但是,如果视图使用的模板也是由插件提供的,那么可能需要以下语法

<g:render template="fooTemplate" plugin="amazon"/>

请注意 plugin 属性的使用,它包含模板所在的插件的名称。如果没有指定,Grails 将在应用程序的相对路径中查找模板。

排除的工件

默认情况下,Grails 在打包过程中排除以下文件

  • grails-app/conf/logback.groovy

  • grails-app/conf/application.yml(重命名为 plugin.yml

  • grails-app/conf/spring/resources.groovy

  • /src/test/*\* 中的所有内容

  • *\*/.svn/*\**\*/CVS/*\* 中的 SCM 管理文件

默认的 UrlMappings.groovy 文件不会被排除,因此请删除任何插件工作不需要的映射。您也可以自由地添加一个不同名称的 UrlMappings 定义,它将**被**包含在内。例如,一个名为 grails-app/controllers/BlogUrlMappings.groovy 的文件是可以的。

排除列表可以通过 pluginExcludes 属性进行扩展

// resources that are excluded from plugin packaging
def pluginExcludes = [
    "grails-app/views/error.gsp"
]

例如,这对于在插件仓库中包含演示或测试资源,但不将它们包含在最终发行版中非常有用。

17.4 评估约定

在研究如何根据约定提供运行时配置之前,您首先需要了解如何从插件中评估这些约定。每个插件都有一个隐式的 application 变量,它是 GrailsApplication 接口的一个实例。

GrailsApplication 接口提供了评估项目中约定并内部存储应用程序中所有工件类引用的方法。

工件实现了 GrailsClass 接口,它表示 Grails 资源,例如控制器或标签库。例如,要获取所有 GrailsClass 实例,您可以执行以下操作

for (grailsClass in application.allClasses) {
    println grailsClass.name
}

GrailsApplication 有一些“神奇的”属性来缩小您感兴趣的工件类型。例如,要访问控制器,您可以使用

for (controllerClass in application.controllerClasses) {
    println controllerClass.name
}

动态方法约定如下

  • *Classes - 检索特定工件名称的所有类。例如 application.controllerClasses

  • get*Class - 检索特定工件的命名类。例如 application.getControllerClass("PersonController")

  • is*Class - 如果给定的类是给定工件类型,则返回 true。例如 application.isControllerClass(PersonController)

GrailsClass 接口有许多有用的方法,可以让您进一步评估和使用这些约定。这些方法包括

  • getPropertyValue - 获取类上给定属性的初始值

  • hasProperty - 如果类具有指定的属性,则返回 true

  • newInstance - 创建此类的新实例。

  • getName - 返回应用程序中类的逻辑名称,如果适用,不带尾随约定部分

  • getShortName - 返回不带包前缀的类的短名称

  • getFullName - 返回应用程序中类的全名,包括尾随约定部分和包名称

  • getPropertyName - 将类的名称作为属性名称返回

  • getLogicalPropertyName - 返回应用程序中类的逻辑属性名称,如果适用,不带尾随约定部分

  • getNaturalName - 以自然语言形式返回属性的名称(例如,'lastName' 变为 'Last Name')

  • getPackageName - 返回包名称

有关完整的参考,请参阅 javadoc API

17.5 连接到运行时配置

Grails 提供了许多钩子来利用系统的不同部分,并通过约定执行运行时配置。

连接到 Grails Spring 配置

首先,您可以连接到 Grails 运行时配置,覆盖 Plugin 类中的 doWithSpring 方法,并返回一个定义其他 bean 的闭包。例如,以下代码段来自提供 i18n 支持的核心 Grails 插件之一

import org.springframework.web.servlet.i18n.CookieLocaleResolver
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor
import org.springframework.context.support.ReloadableResourceBundleMessageSource
import grails.plugins.*

class I18nGrailsPlugin extends Plugin {

    def version = "0.1"

    Closure doWithSpring() {{->
        messageSource(ReloadableResourceBundleMessageSource) {
            basename = "WEB-INF/grails-app/i18n/messages"
        }
        localeChangeInterceptor(LocaleChangeInterceptor) {
            paramName = "lang"
        }
        localeResolver(CookieLocaleResolver)
    }}
}

此插件配置 Grails messageSource bean 和其他几个用于管理区域设置解析和切换的 bean。它使用 Spring Bean Builder 语法来实现这一点。

自定义 Servlet 环境

在以前版本的 Grails 中,可以动态修改生成的 web.xml。在 Grails 3.x 中,没有 web.xml 文件,并且不再可以编程修改 web.xml 文件。

但是,在 Grails 3.x 中,可以执行修改 Servlet 环境最常见的任务。

添加新的 Servlet

如果要添加新的 Servlet 实例,最简单的方法是在 doWithSpring 方法中定义一个新的 Spring bean

Closure doWithSpring() {{->
  myServlet(MyServlet)
}}

如果需要自定义 servlet,可以使用 Spring Boot 的 ServletRegistrationBean

Closure doWithSpring() {{->
  myServlet(ServletRegistrationBean, new MyServlet(), "/myServlet/*") {
    loadOnStartup = 2
  }
}}

添加新的 Servlet 过滤器

与 Servlet 一样,配置新过滤器的最简单方法是简单地定义一个 Spring bean

Closure doWithSpring() {{->
  myFilter(MyFilter)
}}

但是,如果要控制过滤器注册的顺序,则需要使用 Spring Boot 的 FilterRegistrationBean

myFilter(FilterRegistrationBean) {
    filter = bean(MyFilter)
    urlPatterns = ['/*']
    order = Ordered.HIGHEST_PRECEDENCE
}
Grails 的内部注册过滤器(GrailsWebRequestFilterHiddenHttpMethodFilter 等)是通过将 HIGHEST_PRECEDENCE 增加 10 来定义的,因此允许多个过滤器插入到 Grails 过滤器之前或之间。

执行初始化后配置

有时,在构建 Spring ApplicationContext 之后,能够进行一些运行时配置非常有用。在这种情况下,您可以定义一个 doWithApplicationContext 闭包属性。

class SimplePlugin extends Plugin{

    def name = "simple"
    def version = "1.1"

    @Override
    void doWithApplicationContext() {
        def sessionFactory = applicationContext.sessionFactory
        // do something here with session factory
    }
}

17.6 在编译时添加方法

Grails 3.0 可以轻松地从插件向现有工件类型添加新的 trait。例如,假设您想为控制器添加用于操作日期的方法。这可以通过在 src/main/groovy 中定义一个 trait 来完成

package myplugin

@Enhances("Controller")
trait DateTrait {
  Date currentDate() {
    return new Date()
  }
}

@Enhances 注释定义了 trait 应该应用于的工件类型。

作为使用上述 @Enhances 注释的替代方法,您可以实现 TraitInjector 来告诉 Grails 您希望在编译时将 trait 注入哪些工件

package myplugin

@CompileStatic
class ControllerTraitInjector implements TraitInjector {

    @Override
    Class getTrait() {
        SomeTrait
    }

    @Override
    String[] getArtefactTypes() {
        ['Controller'] as String[]
    }
}

上面的 TraitInjector 会将 SomeTrait 添加到所有控制器。getArtefactTypes 方法定义了 trait 应该应用于的工件类型。

有条件地应用 trait

TraitInjector 实现还可以实现 SupportsClassNode 接口,以便仅将 trait 应用于满足自定义要求的工件。例如,如果只有在目标工件类具有特定注释的情况下才应用 trait,则可以按如下方式进行

package myplugin

@CompileStatic
class AnnotationBasedTraitInjector implements TraitInjector, SupportsClassNode {

    @Override
    Class getTrait() {
        SomeTrait
    }

    @Override
    String[] getArtefactTypes() {
        ['Controller'] as String[]
    }

    boolean supports(ClassNode classNode) {
      return GrailsASTUtils.hasAnnotation(classNode, SomeAnnotation)
    }
}

上面的 TraitInjector 将只将 SomeTrait 添加到声明了 SomeAnnotation 的控制器中。

框架通过 .jar 文件中的 META-INF/grails.factories 描述符发现 trait 注入器。此描述符是自动生成的。为上面显示的代码生成的描述符如下所示

#Grails Factories File
grails.compiler.traits.TraitInjector=
myplugin.ControllerTraitInjector,myplugin.DateTraitTraitInjector
由于格式问题,上面的代码片段在等号后包含一个换行符。

该文件是自动生成的,并在构建时添加到 .jar 文件中。如果由于某种原因应用程序在 src/main/resources/META-INF/grails.factories 中定义了自己的 grails.factories 文件,则必须在该文件中显式定义 trait 注入器。只有当应用程序没有定义自己的 src/main/resources/META-INF/grails.factores 文件时,自动生成的元数据才是可靠的。

17.7 在运行时添加动态方法

基础知识

Grails 插件允许您在运行时向任何 Grails 管理的类或其他类注册动态方法。这项工作是在 doWithDynamicMethods 方法中完成的。

请注意,Grails 3.x 具有一些较新的特性,例如可以使用 CompileStatic 编译的代码中的 trait。建议仅在 trait 无法实现的情况下才添加动态行为。
class ExamplePlugin extends Plugin {
    void doWithDynamicMethods() {
        for (controllerClass in grailsApplication.controllerClasses) {
             controllerClass.metaClass.myNewMethod = {-> println "hello world" }
        }
    }
}

在这种情况下,我们使用隐式应用程序对象获取对所有控制器类 MetaClass 实例的引用,并向每个控制器添加一个名为 myNewMethod 的新方法。如果您事先知道要向哪个类添加方法,则可以直接引用其 metaClass 属性。

例如,我们可以向 java.lang.String 添加一个新的 swapCase 方法

class ExamplePlugin extends Plugin  {

    @Override
    void doWithDynamicMethods() {
        String.metaClass.swapCase = {->
             def sb = new StringBuilder()
             delegate.each {
                 sb << (Character.isUpperCase(it as char) ?
                        Character.toLowerCase(it as char) :
                        Character.toUpperCase(it as char))
             }
             sb.toString()
        }

        assert "UpAndDown" == "uPaNDdOWN".swapCase()
    }
}

与 ApplicationContext 交互

doWithDynamicMethods 闭包传递 Spring ApplicationContext 实例。这很有用,因为它允许您与其中的对象进行交互。例如,如果您正在实现一个与 Hibernate 交互的方法,则可以将 SessionFactory 实例与 HibernateTemplate 结合使用

import org.springframework.orm.hibernate3.HibernateTemplate

class ExampleHibernatePlugin extends Plugin{

   void doWithDynamicMethods() {

       for (domainClass in grailsApplication.domainClasses) {

           domainClass.metaClass.static.load = { Long id->
                def sf = applicationContext.sessionFactory
                def template = new HibernateTemplate(sf)
                template.load(delegate, id)
           }
       }
   }
}

此外,由于 Spring 容器的自动装配和依赖注入功能,您可以实现更强大的动态构造函数,这些构造函数使用应用程序上下文在运行时将依赖项连接到对象中

class MyConstructorPlugin {

    void doWithDynamicMethods()
         for (domainClass in grailsApplication.domainClasses) {
              domainClass.metaClass.constructor = {->
                  return applicationContext.getBean(domainClass.name)
              }
         }
    }
}

在这里,我们实际上是用一个查找原型 Spring bean 的构造函数替换了默认构造函数!

17.8 参与自动重新加载事件

监控资源的变化

通常,监控资源的变化并在发生变化时执行某些操作非常有用。这就是 Grails 在运行时实现应用程序状态高级重新加载的方式。例如,请考虑 Grails ServicesPlugin 中的以下简化代码段

class ServicesGrailsPlugin extends Plugin {
    ...
    def watchedResources = "file:./grails-app/services/**/*Service.groovy"

    ...
    void onChange( Map<String, Object> event) {
        if (event.source) {
            def serviceClass = grailsApplication.addServiceClass(event.source)
            def serviceName = "${serviceClass.propertyName}"
            beans {
                "$serviceName"(serviceClass.getClazz()) { bean ->
                    bean.autowire =  true
                }
            }
        }
    }
}

首先,它将 watchedResources 定义为一个字符串或一个字符串列表,其中包含要监视的资源的引用或模式。如果监视的资源指定了一个 Groovy 文件,那么当它发生更改时,它将自动重新加载并传递到 onChange 闭包中的 event 对象中。

event 对象定义了许多有用的属性

  • event.source - 事件源,可以是重新加载的 Class 或 Spring Resource

  • event.ctx - Spring ApplicationContext 实例

  • event.plugin - 管理资源的插件对象(通常是 this

  • event.application - GrailsApplication 实例

  • event.manager - GrailsPluginManager 实例

这些对象可用于帮助您根据更改内容应用适当的更改。在上面的“服务”示例中,当其中一个服务类发生更改时,新的服务 bean 会在 ApplicationContext 中重新注册。

影响其他插件

除了对更改做出反应之外,有时插件还需要“影响”另一个插件。

以服务插件和控制器插件为例。重新加载服务时,除非您也重新加载控制器,否则当您尝试将重新加载的服务自动装配到较旧的控制器类时,会出现问题。

为了解决这个问题,您可以指定另一个插件“影响”哪些插件。这意味着当一个插件检测到更改时,它将重新加载自身,然后重新加载其影响的插件。例如,请考虑 ServicesGrailsPlugin 中的这段代码片段

def influences = ['controllers']

观察其他插件

如果您想观察某个特定插件的更改,但不必监视它所监视的资源,则可以使用“observe”属性

def observe = ["controllers"]

在这种情况下,当控制器发生更改时,您还将收到从控制器插件链接的事件。

插件也可以使用通配符来观察所有加载的插件

def observe = ["*"]

日志插件正是这样做的,以便它可以将 log 属性添加回应用程序运行时更改的*任何*工件。

17.9 了解插件加载顺序

控制插件依赖关系

插件通常依赖于其他插件的存在,并且可以根据其他插件的存在进行调整。这是通过两个属性实现的。第一个称为 dependsOn。例如,请查看 Hibernate 插件中的这段代码片段

class HibernateGrailsPlugin {

    def version = "1.0"

    def dependsOn = [dataSource: "1.0",
                     domainClass: "1.0",
                     i18n: "1.0",
                     core: "1.0"]
}

Hibernate 插件依赖于四个插件的存在:dataSourcedomainClassi18ncore 插件。

依赖项将在 Hibernate 插件之前加载,如果所有依赖项都没有加载,则该插件将不会加载。

dependsOn 属性还支持用于指定版本范围的迷你表达式语言。下面是语法的一些示例

def dependsOn = [foo: "* > 1.0"]
def dependsOn = [foo: "1.0 > 1.1"]
def dependsOn = [foo: "1.0 > *"]

当使用通配符 * 字符时,它表示“任何”版本。表达式语法还排除任何后缀,例如 -BETA、-ALPHA 等,因此例如表达式“1.0 > 1.1”将匹配以下任何版本

  • 1.1

  • 1.0

  • 1.0.1

  • 1.0.3-SNAPSHOT

  • 1.1-BETA2

控制加载顺序

使用 dependsOn 建立了一个“硬”依赖关系,即如果依赖关系未解决,插件将放弃并且不会加载。但是,可以使用 loadAfterloadBefore 属性来建立较弱的依赖关系

def loadAfter = ['controllers']

如果 controllers 插件存在,则该插件将在其之后加载,否则将直接加载。然后,插件可以适应其他插件的存在,例如,Hibernate 插件在其 doWithSpring 闭包中具有以下代码

if (manager?.hasGrailsPlugin("controllers")) {
    openSessionInViewInterceptor(OpenSessionInViewInterceptor) {
        flushMode = HibernateAccessor.FLUSH_MANUAL
        sessionFactory = sessionFactory
    }
    grailsUrlHandlerMapping.interceptors << openSessionInViewInterceptor
}

在这里,Hibernate 插件仅在已加载 controllers 插件时才会注册 OpenSessionInViewInterceptormanager 变量是 GrailsPluginManager 接口的实例,它提供了与其他插件交互的方法。

您还可以使用 loadBefore 属性来指定一个或多个您的插件应该在其之前加载的插件

def loadBefore = ['rabbitmq']

范围和环境

您不仅可以控制插件加载顺序。您还可以指定您的插件应该加载在哪些环境中以及哪些范围(构建阶段)中。只需在您的插件描述符中声明以下一个或两个属性

def environments = ['development', 'test', 'myCustomEnv']
def scopes = [excludes:'war']

在本例中,插件将仅在“开发”和“测试”环境中加载。它也不会打包到 WAR 文件中,因为它在“war”阶段被排除在外。这允许 development-only 插件不会打包用于生产用途。

可用范围的完整列表由枚举 BuildScope 定义,但以下是摘要

  • test - 运行测试时

  • functional-test - 运行功能测试时

  • run - 用于 run-app 和 run-war

  • war - 将应用程序打包为 WAR 文件时

  • all - 插件适用于所有范围(默认)

这两个属性都可以是以下之一

  • 字符串 - 唯一的包含项

  • 列表 - 要包含的环境或范围列表

  • 映射 - 用于完全控制,带有“includes”和/或“excludes”键,它们可以具有字符串或列表值

例如,

def environments = "test"

将仅在测试环境中包含插件,而

def environments = ["development", "test"]

将在开发*和*测试环境中包含它。最后,

def environments = [includes: ["development", "test"]]

将执行相同的操作。

17.10 工件 API

您现在应该了解 Grails 具有工件的概念:它知道的特殊类型的类,并且可以区别于普通的 Groovy 和 Java 类进行处理,例如通过使用额外的属性和方法来增强它们。工件的示例包括域类和控制器。您可能不知道的是,Grails 允许应用程序和插件开发人员访问工件的基础架构,这意味着您可以找出可用的工件,甚至可以自己增强它们。您甚至可以提供自己的自定义工件类型。

17.10.1 查询可用工件

作为插件开发人员,了解应用程序中可用的域类、控制器或其他类型的工件对您来说可能很重要。例如,Elasticsearch 插件 需要知道存在哪些域类,以便它可以检查它们是否有任何 searchable 属性并索引适当的属性。那么它是如何做到的呢?答案在于 grailsApplication 对象,GrailsApplication 的实例,它在控制器和 GSP 中自动可用,并且可以在其他任何地方注入

grailsApplication 对象具有几个用于查询工件的重要属性和方法。最常见的可能是为您提供特定工件类型的所有类的属性

for (cls in grailsApplication.<artefactType>Classes) {
    ...
}

在这种情况下,artefactType 是工件类型的属性名称形式。使用核心 Grails,您拥有

  • domain

  • controller

  • tagLib

  • service

  • codec

  • bootstrap

  • urlMappings

因此,例如,如果要遍历所有域类,则使用

for (cls in grailsApplication.domainClasses) {
    ...
}

对于 URL 映射,则使用

for (cls in grailsApplication.urlMappingsClasses) {
    ...
}

您需要注意的是,这些属性返回的对象不是 Class 的实例。相反,它们是 GrailsClass 的实例,它具有一些特别有用的属性和方法,包括用于底层 Class 的属性和方法

  • shortName - 不带包的工件类名(等效于 Class.simpleName)。

  • logicalPropertyName - 不带“type”后缀的属性形式的工件名称。因此,MyGreatController 变为“myGreat”。

  • isAbstract() - 一个布尔值,指示工件类是否是抽象的。

  • getPropertyValue(name) - 返回给定属性的值,无论它是静态属性还是实例属性。如果属性在声明时初始化,则此方法最有效,例如 static transactional = true

工件 API 还允许您按名称获取类并检查类是否是工件

  • get<type>Class(String name)

  • is<type>Class(Class clazz)

第一种方法将检索给定名称的 GrailsClass 实例,例如“MyGreatController”。第二种方法将检查类是否是特定类型的工件。例如,您可以使用 grailsApplication.isControllerClass(org.example.MyGreatController) 来检查 MyGreatController 是否实际上是一个控制器。

17.10.2 添加您自己的工件类型

插件可以轻松地提供自己的工件,以便它们可以轻松地找出可用的实现并参与重新加载。您需要做的就是创建一个 ArtefactHandler 实现并在您的主插件类中注册它

class MyGrailsPlugin {
    def artefacts = [ org.somewhere.MyArtefactHandler ]
    ...
}

artefacts 列表可以包含处理程序类(如上所示)或处理程序的实例。

那么,工件处理程序是什么样的呢?简而言之,它是 ArtefactHandler 接口的实现。为了使生活更轻松,有一个可以轻松扩展的框架实现:ArtefactHandlerAdapter

除了处理程序本身之外,每个新的工件都需要一个相应的包装器类,该类实现 GrailsClass。同样,框架实现也是可用的,例如 AbstractInjectableGrailsClass,它特别有用,因为它将您的工件变成了一个自动装配的 Spring bean,就像控制器和服务一样。

了解处理程序和包装器类如何工作的最佳方法是查看 Quartz 插件

另一个例子是 Shiro 插件,它添加了一个 realm 工件。