(快速参考)

18 Grails 和 Spring

版本 6.2.0

18 Grails 和 Spring

此部分适用于高级用户和有兴趣了解 Grails 如何与 Spring 框架 集成以及在 Spring 框架基础上构建的那些用户。对于正在考虑对 Grails 执行运行时配置的插件开发者来说,此部分也十分有用。

18.1 配置附加 Bean

借助 Spring Bean DSL

通过在 grails-app/conf/spring/resources.groovy 中配置 Bean,您可以轻松注册新的(或覆盖现有的)Bean,该文件使用 Grails Spring DSL。Bean 在 beans 属性(一个闭包)内定义

beans = {
    // beans here
}

作为示例,您可以使用以下语法配置 Bean

import my.company.MyBeanImpl

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

配置后,可以通过声明一个名为 Bean 名称(在此例中为 myBean)的公共字段,将 Bean 自动注入到 Grails 构件和其他支持依赖注入的类(例如 BootStrap.groovy 和集成测试)

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 文件中定义一个名为先前由 Grails 或已安装插件注册的 Bean,那么您的 Bean 将替换先前的注册。这是一种自定义行为的便捷方法,无需编辑插件代码或采用其他会影响可维护性的方法。

借助 XML

Bean 还可以使用 grails-app/conf/spring/resources.xml 进行配置。在 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 需要一个公共设置器来设置 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 引用看到这一点。

正如在以下代码中所示,Builder 还可以设置与 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 类加载使用此处定义的相同路径匹配语法定义 Bean 的外部 Groovy 脚本。例如

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 的概念,经常从这些工厂而非 Class 的新实例直接创建 bean。在这种情况下,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 名称,因此在使用 ref 方法连接其他 bean 时可能需要按名称引用它们

def beanName = "example"
bb.beans {

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

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

此处使用对 exampleBean 的运行时引用设置 AnotherBean 的示例属性。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,它具有值为 "Lancelot" 的 leader 属性。要使用抽象 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
    }
}

在此示例中,我们创建了一个类型为 KnightOfTheRoundTable 的抽象 bean,并使用 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 属性。

你用 Bean 名称及其值定义一个 beans

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 执行器端点 允许你监控和与你的应用程序进行交互。Spring Boot 包含了许多内置端点。例如,health 端点提供基本的应用程序运行状况信息。

从 Grails 3.1.8 起,默认情况下禁用这些端点。

你可以按照如下方式在你的 application.yml 中启用执行器端点

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