(快速參考)

18 Grails 和 Spring

版本 6.2.0

18 Grails 和 Spring

此區段適用於進階使用者,以及有興趣了解 Grails 如何整合和建構於 Spring Framework 上的人員。對於考慮執行 Grails 執行時期設定的 外掛程式開發人員 也很有用。

18.1 設定其他 Bean

使用 Spring Bean DSL

您可以透過在 grails-app/conf/spring/resources.groovy 中設定 Bean 來輕鬆註冊新的 Bean(或覆寫現有的 Bean),此設定使用 Grails Spring DSL。Bean 定義在 beans 屬性(一個 Closure)內

beans = {
    // beans here
}

以下是一個簡單範例,您可以使用以下語法設定 Bean

import my.company.MyBeanImpl

beans = {
    myBean(MyBeanImpl) {
        someProperty = 42
        otherProperty = "blue"
    }
}

設定後,Bean 可以自動注入到 Grails 工件和其他支援依賴注入的類別(例如 BootStrap.groovy 和整合測試)中,方法是宣告一個公用欄位,其名稱為 Bean 的名稱(在本例中為 myBean

class ExampleController {

     def myBean
     ...
}

使用 DSL 的好處是您可以混合 Bean 宣告和邏輯,例如根據 環境

import grails.util.Environment
import my.company.mock.MockImpl
import my.company.MyBeanImpl

beans = {
    switch(Environment.current) {
        case Environment.PRODUCTION:
            myBean(MyBeanImpl) {
                someProperty = 42
                otherProperty = "blue"
            }
            break

        case Environment.DEVELOPMENT:
            myBean(MockImpl) {
                someProperty = 42
                otherProperty = "blue"
            }
            break
    }
}

可以使用 application 變數存取 GrailsApplication 物件,並用於存取 Grails 設定(以及其他項目)

import grails.util.Environment
import my.company.mock.MockImpl
import my.company.MyBeanImpl

beans = {
    if (application.config.getProperty('my.company.mockService')) {
        myBean(MockImpl) {
            someProperty = 42
            otherProperty = "blue"
        }
    } else {
        myBean(MyBeanImpl) {
            someProperty = 42
            otherProperty = "blue"
        }
    }
}
如果您在 resources.groovy 中定義一個 Bean,其名稱與先前由 Grails 或已安裝外掛程式註冊的名稱相同,您的 Bean 將取代先前的註冊。這是一種自訂行為的便利方式,無需編輯外掛程式程式碼或採用其他會影響可維護性的方式。

使用 XML

也可以使用 grails-app/conf/spring/resources.xml 設定 Bean。在較早版本的 Grails 中,此檔案會由 run-app 腳本自動為您產生,但現在 resources.groovy 中的 DSL 是首選方式,因此現在不會自動產生。不過它仍然受到支援,您只需要自行建立即可。

此檔案是典型的 Spring XML 檔案,Spring 文件有一個 極佳的參考,說明如何設定 Spring Bean。

我們使用 DSL 設定的 myBean bean 會在 XML 檔案中使用以下語法設定

<bean id="myBean" class="my.company.MyBeanImpl">
    <property name="someProperty" value="42" />
    <property name="otherProperty" value="blue" />
</bean>

與其他 bean 一樣,它可以自動注入到支援依賴注入的任何類別中

class ExampleController {

     def myBean
}

參照現有 Bean

resources.groovyresources.xml 中宣告的 Bean 可以依慣例參照其他 Bean。例如,如果您有一個 BookService 類別,它的 Spring bean 名稱將會是 bookService,因此您的 bean 會在 DSL 中像這樣參照它

beans = {
    myBean(MyBeanImpl) {
        someProperty = 42
        otherProperty = "blue"
        bookService = ref("bookService")
    }
}

或在 XML 中像這樣

<bean id="myBean" class="my.company.MyBeanImpl">
    <property name="someProperty" value="42" />
    <property name="otherProperty" value="blue" />
    <property name="bookService" ref="bookService" />
</bean>

bean 需要一個公用的 setter 來設定 bean 參照(以及兩個簡單的屬性),在 Groovy 中會像這樣定義

package my.company

class MyBeanImpl {
    Integer someProperty
    String otherProperty
    BookService bookService // or just "def bookService"
}

或在 Java 中像這樣

package my.company;

class MyBeanImpl {

    private BookService bookService;
    private Integer someProperty;
    private String otherProperty;

    public void setBookService(BookService theBookService) {
        this.bookService = theBookService;
    }

    public void setSomeProperty(Integer someProperty) {
        this.someProperty = someProperty;
    }

    public void setOtherProperty(String otherProperty) {
        this.otherProperty = otherProperty;
    }
}

使用 ref(在 XML 或 DSL 中)非常強大,因為它設定了一個執行時間參照,因此被參照的 bean 不需要存在。只要它在最終應用程式內容設定發生時存在,所有內容都會正確解析。

有關可用 bean 的完整參考,請參閱參考指南中的外掛程式參考。

18.2 使用 Bean DSL 的執行時間 Spring

Grails 中的這個 Bean 建構器旨在提供一種簡化的方式來連接使用 Spring 為核心的依賴項。

此外,Spring 的常規設定方式(透過 XML 和註解)是靜態的,難以在執行時間修改和設定,除了容易出錯且冗長的程式化 XML 建立。Grails 的 BeanBuilder 改變了這一切,讓您可以在執行時間以程式化方式連接組件,讓您可以根據系統屬性或環境變數調整邏輯。

這讓程式碼能夠適應其環境,並避免不必要的程式碼重複(針對測試、開發和生產環境有不同的 Spring 設定)

BeanBuilder 類別

Grails 提供了一個 grails.spring.BeanBuilder 類別,它使用動態 Groovy 來建立 bean 定義。基礎如下

import org.apache.commons.dbcp.BasicDataSource
import org.grails.orm.hibernate.ConfigurableLocalSessionFactoryBean
import org.springframework.context.ApplicationContext
import grails.spring.BeanBuilder

def bb = new BeanBuilder()

bb.beans {

    dataSource(BasicDataSource) {
        driverClassName = "org.h2.Driver"
        url = "jdbc:h2:mem:grailsDB"
        username = "sa"
        password = ""
    }

    sessionFactory(ConfigurableLocalSessionFactoryBean) {
        dataSource = ref('dataSource')
        hibernateProperties = ["hibernate.hbm2ddl.auto": "create-drop",
                               "hibernate.show_sql":     "true"]
    }
}

ApplicationContext appContext = bb.createApplicationContext()
外掛程式grails-app/conf/spring/resources.groovy 檔案中,您不需要建立 BeanBuilder 的新執行個體。相反地,DSL 分別在 doWithSpringbeans 區塊中隱含可用。

此範例顯示如何使用 BeanBuilder 類別設定具有資料來源的 Hibernate。

每個方法呼叫(在本例中為 dataSourcesessionFactory 呼叫)都對應到 Spring 中 bean 的名稱。方法的第一個引數是 bean 的類別,而最後一個引數是一個區塊。在區塊的主體中,你可以使用標準 Groovy 語法設定 bean 的屬性。

bean 參照會使用 bean 的名稱自動解析。這可以在上面的範例中看到,其中 sessionFactory bean 解析 dataSource 參照的方式。

建構器也可以設定某些與 bean 管理相關的特殊屬性,如下列程式碼所示

sessionFactory(ConfigurableLocalSessionFactoryBean) { bean ->
    // Autowiring behaviour. The other option is 'byType'. <<autowire>>
    bean.autowire = 'byName'
    // Sets the initialisation method to 'init'. [init-method]
    bean.initMethod = 'init'
    // Sets the destruction method to 'destroy'. [destroy-method]
    bean.destroyMethod = 'destroy'
    // Sets the scope of the bean. <<scope>>
    bean.scope = 'request'
    dataSource = ref('dataSource')
    hibernateProperties = ["hibernate.hbm2ddl.auto": "create-drop",
                           "hibernate.show_sql":     "true"]
}

方括號中的字串是 Spring XML 定義中對應 bean 屬性的名稱。

在 Spring MVC 中使用 BeanBuilder

在你的類別路徑中包含 grails-spring-<version>.jar 檔案,以便在一般的 Spring MVC 應用程式中使用 BeanBuilder。然後將下列 <context-param> 值新增到你的 /WEB-INF/web.xml 檔案

<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>/WEB-INF/applicationContext.groovy</param-value>
</context-param>

<context-param>
    <param-name>contextClass</param-name>
    <param-value>
      grails.web.servlet.context.GrailsWebApplicationContext
    </param-value>
</context-param>

然後建立一個 /WEB-INF/applicationContext.groovy 檔案來完成剩下的工作

import org.apache.commons.dbcp.BasicDataSource

beans {
    dataSource(BasicDataSource) {
        driverClassName = "org.h2.Driver"
        url = "jdbc:h2:mem:grailsDB"
        username = "sa"
        password = ""
    }
}

從檔案系統載入 Bean 定義

你可以使用 BeanBuilder 類別載入外部 Groovy 程式碼,這些程式碼使用這裡定義的相同路徑比對語法來定義 bean。例如

def bb = new BeanBuilder()
bb.loadBeans("classpath:*SpringBeans.groovy")

def applicationContext = bb.createApplicationContext()

在這裡,BeanBuilder 會載入類別路徑上所有以 SpringBeans.groovy 結尾的 Groovy 檔案,並將它們解析成 bean 定義。範例程式碼如下所示

import org.apache.commons.dbcp.BasicDataSource
import org.grails.orm.hibernate.ConfigurableLocalSessionFactoryBean

beans {

    dataSource(BasicDataSource) {
        driverClassName = "org.h2.Driver"
        url = "jdbc:h2:mem:grailsDB"
        username = "sa"
        password = ""
    }

    sessionFactory(ConfigurableLocalSessionFactoryBean) {
        dataSource = dataSource
        hibernateProperties = ["hibernate.hbm2ddl.auto": "create-drop",
                               "hibernate.show_sql":     "true"]
    }
}

將變數新增到繫結(內容)

如果你從程式碼載入 bean,你可以建立一個 Groovy Binding 來設定要使用的繫結

def binding = new Binding()
binding.maxSize = 10000
binding.productGroup = 'finance'

def bb = new BeanBuilder()
bb.binding = binding
bb.loadBeans("classpath:*SpringBeans.groovy")

def ctx = bb.createApplicationContext()

然後你可以在 DSL 檔案中存取 maxSizeproductGroup 屬性。

18.3 BeanBuilder DSL 說明

使用建構函數引數

建構函數引數可以使用每個定義 bean 的方法的參數來定義。將它們放在第一個引數(類別)之後

bb.beans {
    exampleBean(MyExampleBean, "firstArgument", 2) {
        someProperty = [1, 2, 3]
    }
}

此設定對應到一個 MyExampleBean,其建構函數如下所示

MyExampleBean(String foo, int bar) {
   ...
}

設定 BeanDefinition(使用工廠方法)

閉包的第一個引數是對 bean 設定實例的參照,你可以使用它來設定工廠方法,並呼叫 AbstractBeanDefinition 類別中的任何方法

bb.beans {
    exampleBean(MyExampleBean) { bean ->
        bean.factoryMethod = "getInstance"
        bean.singleton = false
        someProperty = [1, 2, 3]
    }
}

或者,你也可以使用定義 bean 的方法的傳回值來設定 bean

bb.beans {
    def example = exampleBean(MyExampleBean) {
        someProperty = [1, 2, 3]
    }
    example.factoryMethod = "getInstance"
}

使用工廠 bean

Spring 定義了工廠 Bean 的概念,通常 Bean 不是直接從 Class 的新實例建立,而是從這些工廠建立。在這種情況下,Bean 沒有 Class 參數,而必須將工廠 Bean 的名稱傳遞給 Bean 定義方法

bb.beans {

    myFactory(ExampleFactoryBean) {
        someProperty = [1, 2, 3]
    }

    myBean(myFactory) {
        name = "blah"
    }
}

另一種常見的方法是提供工廠方法的名稱,以呼叫工廠 Bean。這可以使用 Groovy 的命名參數語法來完成

bb.beans {

    myFactory(ExampleFactoryBean) {
        someProperty = [1, 2, 3]
    }

    myBean(myFactory: "getInstance") {
        name = "blah"
    }
}

這裡會呼叫 ExampleFactoryBean Bean 上的 getInstance 方法,以建立 myBean Bean。

在執行階段建立 Bean 參照

有時候你不知道要在執行階段建立哪個 Bean 的名稱。在這種情況下,你可以使用字串內插法動態呼叫 Bean 定義方法

def beanName = "example"
bb.beans {
    "${beanName}Bean"(MyExampleBean) {
        someProperty = [1, 2, 3]
    }
}

在這種情況下,呼叫 Bean 定義方法時會使用先前定義的 beanName 變數。範例有一個硬編碼值,但也可以使用根據設定、系統屬性等以程式方式產生的名稱。

此外,由於有時候 Bean 名稱在執行階段之前並不知道,因此在將其他 Bean 連結在一起時,你可能需要透過名稱參照它們,在這種情況下使用 ref 方法

def beanName = "example"
bb.beans {

    "${beanName}Bean"(MyExampleBean) {
        someProperty = [1, 2, 3]
    }

    anotherBean(AnotherBean) {
        example = ref("${beanName}Bean")
    }
}

這裡 AnotherBean 的範例屬性使用對 exampleBean 的執行階段參照來設定。ref 方法也可以用來參照 BeanBuilder 建構函式中提供的父 ApplicationContext 中的 Bean

ApplicationContext parent = ...//
def bb = new BeanBuilder(parent)
bb.beans {
    anotherBean(AnotherBean) {
        example = ref("${beanName}Bean", true)
    }
}

這裡第二個參數 true 指定參照會在父內容中尋找 Bean。

使用匿名 (內部) Bean

你可以透過將 Bean 的屬性設定為區塊來使用匿名內部 Bean,該區塊會取得一個參數,即 Bean 類型

bb.beans {

    marge(Person) {
        name = "Marge"
        husband = { Person p ->
            name = "Homer"
            age = 45
            props = [overweight: true, height: "1.8m"]
        }
        children = [ref('bart'), ref('lisa')]
    }

    bart(Person) {
        name = "Bart"
        age = 11
    }

    lisa(Person) {
        name = "Lisa"
        age = 9
    }
}

在上述範例中,我們將 marge Bean 的丈夫屬性設定為建立內部 Bean 參照的區塊。或者,如果你有一個工廠 Bean,你可以省略類型,而只使用指定的 Bean 定義來設定工廠

bb.beans {

    personFactory(PersonFactory)

    marge(Person) {
        name = "Marge"
        husband = { bean ->
            bean.factoryBean = "personFactory"
            bean.factoryMethod = "newInstance"
            name = "Homer"
            age = 45
            props = [overweight: true, height: "1.8m"]
        }
        children = [ref('bart'), ref('lisa')]
    }
}

抽象 Bean 和父 Bean 定義

若要建立抽象 Bean 定義,請定義沒有 Class 參數的 Bean

class HolyGrailQuest {
    def start() { println "lets begin" }
}
class KnightOfTheRoundTable {

    String name
    String leader
    HolyGrailQuest quest

    KnightOfTheRoundTable(String name) {
        this.name = name
    }

    def embarkOnQuest() {
        quest.start()
    }
}
import grails.spring.BeanBuilder

def bb = new BeanBuilder()
bb.beans {
    abstractBean {
        leader = "Lancelot"
    }
    ...
}

這裡我們定義一個抽象 Bean,它有一個 leader 屬性,其值為 "Lancelot"。若要使用抽象 Bean,請將其設定為子 Bean 的父項

bb.beans {
    ...
    quest(HolyGrailQuest)

    knights(KnightOfTheRoundTable, "Camelot") { bean ->
        bean.parent = abstractBean
        quest = ref('quest')
    }
}
使用父 Bean 時,你必須在設定 Bean 上的任何其他屬性之前設定 Bean 的父屬性!

如果你想要一個指定 Class 的抽象 Bean,你可以這樣做

import grails.spring.BeanBuilder

def bb = new BeanBuilder()
bb.beans {

    abstractBean(KnightOfTheRoundTable) { bean ->
        bean.'abstract' = true
        leader = "Lancelot"
    }

    quest(HolyGrailQuest)

    knights("Camelot") { bean ->
        bean.parent = abstractBean
        quest = quest
    }
}

在此範例中,我們建立一個抽象 bean,其類型為 KnightOfTheRoundTable,並使用 bean 參數將其設定為抽象。稍後,我們定義一個沒有定義 Class 的 knights bean,但從父 bean 繼承 Class

使用 Spring 名稱空間

自 Spring 2.0 以來,Spring 使用者透過 XML 名稱空間更輕鬆地存取主要功能。您可以在 BeanBuilder 中使用 Spring 名稱空間,方法是使用此語法宣告它

xmlns context:"http://www.springframework.org/schema/context"

然後呼叫與 Spring 名稱空間標籤及其關聯屬性名稱相符的方法

context.'component-scan'('base-package': "my.company.domain")

您可以使用 Spring 名稱空間執行一些有用的操作,例如查詢 JNDI 資源

xmlns jee:"http://www.springframework.org/schema/jee"

jee.'jndi-lookup'(id: "dataSource", 'jndi-name': "java:comp/env/myDataSource")

此範例將透過對指定的 JNDI 名稱執行 JNDI 查詢,建立具有識別碼 dataSource 的 Spring bean。使用 Spring 名稱空間,您還可以從 BeanBuilder 完整存取 Spring 中所有強大的 AOP 支援。例如,給定這兩個類別

class Person {

    int age
    String name

    void birthday() {
        ++age;
    }
}
class BirthdayCardSender {

    List peopleSentCards = []

    void onBirthday(Person person) {
        peopleSentCards << person
    }
}

您可以定義一個切面,它使用切入點來偵測 birthday() 方法何時被呼叫

xmlns aop:"http://www.springframework.org/schema/aop"

fred(Person) {
    name = "Fred"
    age = 45
}

birthdayCardSenderAspect(BirthdayCardSender)

aop {
    config("proxy-target-class": true) {
        aspect(id: "sendBirthdayCard", ref: "birthdayCardSenderAspect") {
            after method: "onBirthday",
            pointcut: "execution(void ..Person.birthday()) and this(person)"
        }
    }
}

18.4 屬性佔位符組態

Grails 支援透過 Spring 的 PropertyPlaceholderConfigurer 延伸版本來進行屬性佔位符組態。

ConfigSlurper 腳本或 Java 屬性檔中定義的設定可以用作 grails-app/conf/spring/resources.xmlgrails-app/conf/spring/resources.groovy 中 Spring 組態的佔位符值。例如,給定 grails-app/conf/application.groovy (或外部化組態) 中的下列項目

database.driver="com.mysql.jdbc.Driver"
database.dbname="mysql:mydb"

然後,您可以使用熟悉的 ${..} 語法,如下所示在 resources.xml 中指定佔位符

<bean id="dataSource"
      class="org.springframework.jdbc.datasource.DriverManagerDataSource">
    <property name="driverClassName">
        <value>${database.driver}</value>
    </property>
    <property name="url">
        <value>jdbc:${database.dbname}</value>
    </property>
 </bean>

要在 resources.groovy 中指定佔位符,您需要使用單引號

dataSource(org.springframework.jdbc.datasource.DriverManagerDataSource) {
    driverClassName = '${database.driver}'
    url = 'jdbc:${database.dbname}'
}

這會將屬性值設定為文字字串,稍後 Spring 的 PropertyPlaceholderConfigurer 會針對組態解析此字串。

resources.groovy 的較佳選項是透過 grailsApplication 變數存取屬性

dataSource(org.springframework.jdbc.datasource.DriverManagerDataSource) {
    driverClassName = grailsApplication.config.getProperty('database.driver', String)
    url = "jdbc\:${grailsApplication.config.getProperty('database.dbname', String)}"
}

使用此方法會讓型別維持在組態中定義的狀態。

18.5 屬性覆寫組態

Grails 支援透過 組態 設定 bean 屬性。

您定義一個 beans 區塊,其中包含 bean 的名稱及其值

beans {
    bookService {
        webServiceURL = "http://www.amazon.com"
    }
}

一般格式為

<<bean name>>.<<property name>> = <<value>>

Java 屬性檔中的相同組態為

beans.bookService.webServiceURL=http://www.amazon.com

18.6 Spring Boot 致動器

Spring Boot Actuator 端點 讓您可以監控和與您的應用程式互動。Spring Boot 包含許多內建端點。例如,health 端點提供基本的應用程式健康資訊。

這些端點在 Grails 3.1.8 之後預設已停用。

您可以在 application.yml 中啟用 actuator 端點,如下所示

grails-app/conf/application.yml
management:
  endpoints:
    enabled-by-default: true