(快速參考)

17 外掛程式

版本 6.2.0

17 外掛程式

Grails 首先且最重要的是一個 Web 應用程式框架,但它也是一個平台。透過公開許多延伸點,讓您能延伸從命令列介面到執行時期設定引擎的任何內容,Grails 可以自訂以符合幾乎任何需求。若要連接到此平台,您只需建立一個外掛程式即可。

擴充平台聽起來很複雜,但外掛的範圍從極其簡單到難以置信的強大都有。如果您知道如何建置 Grails 應用程式,您就會知道如何建立一個外掛來分享資料模型或一些靜態資源。

17.1 建立和安裝外掛

建立外掛

建立 Grails 外掛是執行指令的簡單問題

grails create-plugin <<PLUGIN NAME>>

這會為您指定的 name 建立一個 web-plugin 專案。例如,執行 grails create-plugin example 會建立一個名為 example 的新 web-plugin 專案。

在 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 應用程式專案相同,只不過在 plugin 套件結構下的 src/main/groovy 目錄中,您會找到一個外掛描述符類別 (一個以「GrailsPlugin」結尾的類別)。例如

import grails.plugins.*

class ExampleGrailsPlugin extends Plugin {
   ...
}

所有外掛都必須在 src/main/groovy 目錄下有這個類別,否則它們不會被視為外掛。外掛類別定義關於外掛的元資料,以及各種選用的掛鉤進入外掛擴充點 (稍後涵蓋)。

您也可以使用多個特殊屬性提供關於您的外掛的其他資訊

  • title - 您的外掛的簡短一句話描述

  • grailsVersion - 外掛支援的 Grails 版本範圍。例如,「1.2 > *」(表示 1.2 或更高)

  • author - 外掛作者的名稱

  • authorEmail - 外掛作者的聯絡電子郵件

  • 開發人員 - 除上述作者外,任何其他開發人員。

  • 說明 - 外掛功能的完整多行說明

  • 文件 - 外掛文件網址

  • 授權 - 外掛授權

  • 問題管理 - 外掛問題追蹤器

  • 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 = "http://grails.org/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),請使用 Spring Boot 設定 bean (或 POJO),並加上 ConfigurationProperties 註解。以下是外掛設定範例

./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
}

你可以將 MailPluginConfiguration bean 注入到你的 bean 中,就像其他 bean 一樣。

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

package example

class MailService {

    MailPluginConfiguration mailPluginConfiguration

    void sendMail() {

    }

}

請閱讀 Spring Boot 外部化設定 章節,以取得更多資訊。

安裝本機外掛

若要將 Grails 外掛安裝到你的本機 Maven,你可以使用 Gradle Maven 發布 外掛。你可能還需要設定發布擴充套件,如下所示

publishing {
    publications {
        maven(MavenPublication) {
            versionMapping {
                usage('java-api') {
                    fromResolutionOf('runtimeClasspath')
                }
                usage('java-runtime') {
                    fromResolutionResult()
                }
            }
            from components.java
        }
    }
}
請參閱 Gradle Maven 發布外掛文件,以取得最新資訊。

若要讓你的外掛可以在 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:設定外掛以啟用重新載入

在 plugin 目錄中,新增或修改 gradle.properties 檔案。必須設定新的屬性 exploded=true,才能讓外掛將 exploded 目錄新增到 classpath。

步驟 5:執行應用程式

現在,使用 ./gradlew bootRun 指令從應用程式目錄的根目錄執行應用程式,您可以使用 verbose 旗標查看 Gradle 輸出

$ cd myapp
$ ./gradlew bootRun --verbose

您將會從 Gradle 輸出中注意到外掛來源已建置,並放置在應用程式的 classpath 中

: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://127.0.0.1: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 中央外掛儲存庫中散佈外掛

散佈外掛的建議方式是發佈到官方的 Grails 中央外掛儲存庫。這會讓您的外掛對 list-plugins 指令可見

grails list-plugins

它會列出中央儲存庫中的所有外掛。您的外掛也會對 plugin-info 指令可用

grails plugin-info [plugin-name]

它會列印關於外掛的額外資訊,例如說明、撰寫者等。

如果您已建立 Grails 外掛,並希望將它寄存於中央儲存庫,您可以在 外掛入口網站 找到取得帳戶的說明。

17.3 提供基本工件

加入指令行指令

外掛可以透過兩種方式將新指令加入 Grails 3.0 的互動式外殼。首先,使用 create-script,您可以建立程式碼產生指令碼,它將對應用程式可用。create-script 指令會在 src/main/scripts 目錄中建立指令碼

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

程式碼產生指令碼可以用於在專案樹中建立工件,並自動化與 Gradle 的互動。

如果您想建立與已載入 Grails 應用程式執行個體互動的新外殼指令,您應該使用 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 會建立一個外殼指令和一個 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,來覆寫外掛程式提供的檢視。

例如,考慮由「amazon」外掛程式提供的控制器 BookController。如果執行的動作是 list,Grails 會先尋找名為 grails-app/views/book/list.gsp 的檢視,如果找不到,就會在與外掛程式相關的位置尋找相同的檢視。

但是,如果檢視使用外掛程式也提供的範本,則可能需要下列語法

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

請注意 plugin 屬性的用法,其中包含範本所在的 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,以管理 Locale 解析和切換。它使用 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 讓您可以輕鬆地從外掛中,為現有的工件類型新增新的特質。例如,假設您想為控制器新增用於處理日期的方法。這可以透過在 src/main/groovy 中定義特質來完成

package myplugin

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

@Enhances 標註定義特質應套用於哪種類型的工件。

除了使用上述的 @Enhances 標註,您也可以實作 TraitInjector,以在編譯時告知 Grails 您想要將特質注入哪些工件

package myplugin

@CompileStatic
class ControllerTraitInjector implements TraitInjector {

    @Override
    Class getTrait() {
        SomeTrait
    }

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

上述 TraitInjector 會將 SomeTrait 加入所有控制器。getArtefactTypes 方法定義特質應套用於哪種類型的工件。

有條件地套用特質

TraitInjector 實作也可以實作 SupportsClassNode 介面,以僅將特質套用於符合自訂需求的工件。例如,如果特質僅應套用於目標工件類別具有特定標註時,可以如下所示執行

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 描述符來偵測特質注入器。此描述符會自動產生。針對上述程式碼顯示的描述符會如下所示

#Grails Factories File
grails.compiler.traits.TraitInjector=
myplugin.ControllerTraitInjector,myplugin.DateTraitTraitInjector
由於格式設定問題,上述程式碼片段在等號後包含換行符號。

該檔案會自動產生,並在建置時加入 .jar 檔案。如果應用程式在 src/main/resources/META-INF/grails.factories 中定義自己的 grails.factories 檔案,則必須明確定義該檔案中的特質注入器。自動產生的元資料僅在應用程式未定義自己的 src/main/resources/META-INF/grails.factores 檔案時才可靠。

17.7 在執行階段新增動態方法

基礎知識

Grails 外掛讓您可以在執行階段註冊任何 Grails 管理或其他類別的動態方法。這項工作是在 doWithDynamicMethods 方法中完成。

請注意,Grails 3.x 提供較新的功能,例如可從使用 CompileStatic 編譯的程式碼中使用的特質。建議僅針對無法使用特質處理的情況新增動態行為。
class ExamplePlugin extends Plugin {
    void doWithDynamicMethods() {
        for (controllerClass in grailsApplication.controllerClasses) {
             controllerClass.metaClass.myNewMethod = {-> println "hello world" }
        }
    }
}

在這個案例中,我們使用隱含的應用程式物件取得所有控制器類別的 MetaClass 實例的參考,並將稱為 myNewMethod 的新方法加入每個控制器。如果您事先知道要加入方法的類別,您可以直接參考其 metaClass 屬性。

例如,我們可以新增一個新方法 swapCasejava.lang.String

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 檔案,則在變更檔案時,它會自動重新載入,並傳遞到 event 物件中的 onChange 閉包。

event 物件定義了許多有用的屬性

  • event.source - 事件來源,可能是重新載入的 Class 或 Spring Resource

  • event.ctx - Spring ApplicationContext 實例

  • event.plugin - 管理資源的 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
}

這裡,如果已載入 controllers 外掛程式,Hibernate 外掛程式只會註冊一個 OpenSessionInViewInterceptormanager 變數是 GrailsPluginManager 介面的執行個體,它提供方法與其他外掛程式互動。

您也可以使用 loadBefore 屬性來指定您的外掛程式應該在之前載入的一個或多個外掛程式

def loadBefore = ['rabbitmq']

範圍和環境

您不僅可以控制外掛載入順序。您還可以指定外掛應載入的環境和範圍(建置階段)。只要在外掛描述檔中宣告這兩個屬性中的其中一個或兩個即可

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

在此範例中,外掛只會在「開發」和「測試」環境中載入。它也不會封裝到 WAR 檔案中,因為它已從「war」階段中排除。這允許僅限開發外掛不會封裝以供生產使用。

可用範圍的完整清單由列舉 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 外掛 需要知道有哪些網域類別存在,以便檢查它們是否有任何可搜尋屬性,並索引適當的屬性。那麼它如何執行?答案在於grailsApplication物件,即 GrailsApplication 的執行個體,它會在控制器和 GSP 中自動提供,而且可以在其他任何地方注入

grailsApplication 物件有幾個用於查詢人工製品的重要屬性和方法。最常見的可能是提供特定人工製品類型所有類別的那個

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

在此情況下,artefactType 是工件類型的屬性名稱形式。使用核心 Grails 時,您有

  • 網域

  • 控制器

  • 標籤庫

  • 服務

  • 編解碼器

  • 開機

  • 網址對應

因此,例如,如果您想要遍歷所有網域類別,請使用

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

以及網址對應

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

您需要知道這些屬性傳回的物件不是 Class 的執行個體。相反地,它們是 GrailsClass 的執行個體,它有一些特別有用的屬性和方法,包括一個用於基礎 Class 的屬性

  • shortName - 沒有套件的工件類別名稱(等同於 Class.simpleName)。

  • logicalPropertyName - 沒有「類型」字尾的屬性形式工件名稱。因此,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 外掛程式,它新增一個領域人工製品。