(快速参考)

12 服务层

版本 6.2.0

12 服务层

Grails 定义了服务层概念。Grails 团队不建议在控制器内嵌入核心应用程序逻辑,因为这样会降低可重用性,不利于明确分工。

Grails 中的服务是放置应用程序中大部分逻辑的场所,让控制器负责使用重定向处理请求流等工作。

创建服务

在终端窗口中从项目的根目录运行 create-service 命令可以创建一个 Grails 服务

grails create-service helloworld.simple
如果 create-service 脚本中未指定程序包,Grails 会自动使用在 grails-app/conf/application.yml 中定义的 grails.defaultPackage 作为程序包名。

上述示例将在 grails-app/services/helloworld/SimpleService.groovy 位置创建一个服务。除采用 Service 这种命名约定外,服务就是一个普通的 Groovy 类

package helloworld

class SimpleService {
}

12.1 声明式事务

声明式事务

服务通常涉及协作 领域类 的逻辑,因此通常参与涉及大型操作的持久化。鉴于服务自身的性质,它们常常需要事务行为。你可以使用 withTransaction 方法使用编程方式事务,不过这既重复又无法充分利用 Spring 底层事务抽象的强大功能。

服务启用了事务分界机制,这是一种声明性的方式,用于定义要进行事务处理的方法。若要在服务上启用事务,可以使用 Transactional 转换。

import grails.gorm.transactions.*

@Transactional
class CountryService {

}

结果是所有方法都包裹在一个事务中,如果方法抛出异常(包括检查型异常或运行时异常)或错误,将自动回滚。事务的传播级别默认为 PROPAGATION_REQUIRED

Grails 3.2.0 版本是第一个默认使用 GORM 6 的版本。在 GORM 6 之前,检查型异常不会回滚事务。只有在抛出运行时异常的方法(即扩展了 RuntimeException 的方法)才会回滚事务。
警告:依赖注入 是声明性事务工作的唯一方式。如果你使用 new 运算符,例如 new BookService(),则不会获得事务服务。

Transactional 注释和 transactional 属性

在 Grails 3.1 之前的 Grails 版本中,Grails 创建了 Spring 代理并使用 transactional 属性启用和禁用代理创建。在使用 Grails 3.1 及更高版本创建的应用程序中,默认情况下,这些代理被禁用,支持 @Transactional 转换。

对于 Grails 3.1.x 和 3.2.x 版本,如果你希望重新启用此功能(不推荐),则必须将 grails.spring.transactionManagement 设置为 true 或移除 grails-app/conf/application.ymlgrails-app/conf/application.groovy 中的配置。

在 Grails 3.3.x 中,Spring 代理已完全停止用于事务管理,而且你必须使用 Grails 的 AST 转换。在 Grails 3.3.x 中,如果你希望继续使用 Spring 代理进行事务管理,则必须使用适当的 Spring 配置手动配置它们。

另外,在 Grails 3.1 之前,服务默认是事务性的;而从 Grails 3.1 开始,只有在应用 @Transactional 转换时它们才是事务性的。

自定义事务配置

对于需要在方法级别对事务进行更细粒度的控制或需要指定备用传播级别的情况,Grails 还提供了 @Transactional@NotTransactional 注释。例如,@NotTransactional 注释可用于标记在类使用 @Transactional 进行注释时要跳过的方法。

使用 Transactional 来注释一项服务方法,将禁用该服务默认的 Grails 事务行为(与添加 transactional=false 相同),因此如果您使用任何注释,您必须注释所有需要事务的方法。

在示例中,listBooks 使用只读事务,updateBook 使用默认的读写事务,并且 deleteBook 是非事务性的(根据其名称来看可能不是一个好主意)。

import grails.gorm.transactions.Transactional

class BookService {

    @Transactional(readOnly = true)
    def listBooks() {
        Book.list()
    }

    @Transactional
    def updateBook() {
        // ...
    }

    def deleteBook() {
        // ...
    }
}

您还可以对类进行注释以定义整个服务的默认事务行为,然后覆盖每个方法的默认事务行为

import grails.gorm.transactions.Transactional

@Transactional
class BookService {

    def listBooks() {
        Book.list()
    }

    def updateBook() {
        // ...
    }

    def deleteBook() {
        // ...
    }
}

此版本默认为所有方法都是读写事务性的(由于类级别注释),但 listBooks 方法会覆盖此默认值以使用只读事务

import grails.gorm.transactions.Transactional

@Transactional
class BookService {

    @Transactional(readOnly = true)
    def listBooks() {
        Book.list()
    }

    def updateBook() {
        // ...
    }

    def deleteBook() {
        // ...
    }
}

尽管在此示例中 updateBookdeleteBook 未被注释,但它们继承了来自类级别注释的配置。

有关更多信息,请参阅 Spring 用户指南的 使用 @Transactional 一节。

与 Spring 不同,您不需要任何先前的配置来使用 Transactional; 只需根据需要指定注释,Grails 就会自动检测到它们。

事务状态

Grails 事务服务方法中默认提供一个 TransactionStatus 实例。

示例

import grails.gorm.transactions.Transactional

@Transactional
class BookService {

    def deleteBook() {
        transactionStatus.setRollbackOnly()
    }
}

12.1.1 事务和多数据源

给出两个域类,例如

class Movie {
    String title
}
class Book {
    String title

    static mapping = {
        datasource 'books'
    }
}

您可以向 @Transactional@ReadOnly 注释提供所需的数据源。

import grails.gorm.transactions.ReadOnly
import grails.gorm.transactions.Transactional
import groovy.transform.CompileStatic

@CompileStatic
class BookService {

    @ReadOnly('books')
    List<Book> findAll() {
        Book.where {}.findAll()
    }

    @Transactional('books')
    Book save(String title) {
        Book book = new Book(title: title)
        book.save()
        book
    }
}
@CompileStatic
class MovieService {

    @ReadOnly
    List<Movie> findAll() {
        Movie.where {}.findAll()
    }
}

12.1.2 事务回滚和会话

了解事务和 Hibernate 会话

使用事务时,您必须考虑 Hibernate 如何处理基础持久化会话。事务回滚时,GORM 使用的 Hibernate 会话会被清除。这意味着会话中的任何对象都将分离,并且访问未初始化的延迟加载集合将导致 LazyInitializationException

为了理解清除 Hibernate 会话为何如此重要。请考虑以下示例

class Author {
    String name
    Integer age

    static hasMany = [books: Book]
}

如果您使用连续的事务保存两位作者,如下所示

Author.withTransaction { status ->
    new Author(name: "Stephen King", age: 40).save()
    status.setRollbackOnly()
}

Author.withTransaction { status ->
    new Author(name: "Stephen King", age: 40).save()
}

由于第一个事务通过清除 Hibernate 会话来回滚作者的 save(),因此只能保存第二个作者。如果 Hibernate 会话未被清除,则两个作者实例都将被保留,并且会导致非常意外的结果。

然而,由于会话被清除,而导致 LazyInitializationException 可能会令人沮丧。

例如,请考虑以下示例

class AuthorService {

    void updateAge(id, int age) {
        def author = Author.get(id)
        author.age = age
        if (author.isTooOld()) {
            throw new AuthorException("too old", author)
        }
    }
}
class AuthorController {

    def authorService

    def updateAge() {
        try {
            authorService.updateAge(params.id, params.int("age"))
        }
        catch(e) {
            render "Author books ${e.author.books}"
        }
    }
}

在上述示例中,如果某个 Author 的年龄超出了 isTooOld() 方法中定义的最大值,那么将回滚该事务,方法是引发 AuthorExceptionAuthorException 引用了作者,但是在访问 books 关联关系时会引发 LazyInitializationException,原因是已清除了底层的 Hibernate 会话。

若要解决这个问题,你可以采取多种方法。一种方法是确保以预读的方式查询以获取你所需的数据

class AuthorService {
    ...
    void updateAge(id, int age) {
        def author = Author.findById(id, [fetch:[books:"eager"]])
        ...

在该示例中,会在检索 Author 时对 books 关联关系进行查询。

这是最优解决方案,因为它需要的查询比以下建议的解决方案更少。

另一种解决方案是在回滚事务后重定向请求

class AuthorController {

    AuthorService authorService

    def updateAge() {
        try {
            authorService.updateAge(params.id, params.int("age"))
        }
        catch(e) {
            flash.message = "Can't update age"
            redirect action:"show", id:params.id
        }
    }
}

在此情况下,会使用新的请求再次处理检索 Author 的过程。最后,第三种解决方案是再次检索 Author 的数据,以确保会话保持在正确状态

class AuthorController {

    def authorService

    def updateAge() {
        try {
            authorService.updateAge(params.id, params.int("age"))
        }
        catch(e) {
            def author = Author.read(params.id)
            render "Author books ${author.books}"
        }
    }
}

验证错误和回滚

一个常见用例是在存在验证错误时回滚事务。例如,考虑此服务

import grails.validation.ValidationException

class AuthorService {

    void updateAge(id, int age) {
        def author = Author.get(id)
        author.age = age
        if (!author.validate()) {
            throw new ValidationException("Author is not valid", author.errors)
        }
    }
}

若要重新呈现回滚了事务的相同视图,可以在呈现之前将这些错误重新关联到已刷新的实例

import grails.validation.ValidationException

class AuthorController {

    def authorService

    def updateAge() {
        try {
            authorService.updateAge(params.id, params.int("age"))
        }
        catch (ValidationException e) {
            def author = Author.read(params.id)
            author.errors = e.errors
            render view: "edit", model: [author:author]
        }
    }
}

12.2 带范围的服务

默认情况下,对服务方法的访问不会同步,因此没有任何内容可以阻止这些方法的并发执行。实际上,由于服务是一个单例,而且可以并发使用,因此你应该非常小心地将状态存储在一个服务中。或者选取轻松(且更好的)途径,那就是永远不在服务中存储状态。

你可以通过将服务放置在特定范围中来更改此行为。支持的范围为

  • prototype - 每次将一个服务注入到另一类时都会创建一个新服务

  • request - 每个请求都会创建一个新的服务

  • flash - 仅为当前和下一个请求创建一个新服务

  • flow - 在 Web 流程中,服务将在流程的范围内存在

  • conversation - 在 Web 流程中,服务将在对话的范围内存在。即一个根流程及其子流程

  • session - 为用户会话的范围创建一个服务

  • singleton(默认) - 仅存在服务的一个实例

如果你的服务有 flashflowconversation 范围,则它必须实现 java.io.Serializable,并且只能在 Web 流程的上下文中使用。

若要启用其中一个范围,可向你的类添加一个 static 作用域属性,其值为上述值之一,例如

static scope = "flow"
升级

从 Grails 2.3 开始,新应用程序生成时配置为将控制器的作用域默认为 singleton。如果 singleton 控制器与 prototype 作用域服务交互,则该服务实际上表现为每个控制器单例。如果需要非单例服务,则也应当更改控制器作用域。

有关详细信息,请参见用户指南中的 控制器和作用域

延迟初始化

你还可以配置服务是延迟初始化的还是非延迟的。默认情况下,此项设置为 true,但是你可以使用 lazyInit 属性禁用它并使初始化变得迫切。

static lazyInit = false

12.3 依赖注入和服务

依赖注入基础

Grails 服务的一个关键方面是能够使用 Spring Framework 的依赖注入功能。Grails 支持“约定依赖注入”。换句话说,你可以使用服务类名的属性名称表示形式,将其自动注入到控制器、标签库,等等。

例如,给定一个名为 BookService 的服务,如果你在控制器中像下面这样定义一个名为 bookService 的属性

class BookController {
    def bookService
    ...
}

在这种情况下,Spring 容器将自动根据其配置的作用域注入该服务的一个实例。所有依赖注入都是按名称进行的。你还可以像下面这样指定类型

class AuthorService {
    BookService bookService
}
注意:通常,属性名称是通过将类型的首字母小写而生成的。例如,BookService 类的实例将映射到名为 bookService 的属性。

为了与标准的 JavaBean 约定保持一致,如果类名的首 2 个字母是大写,那么属性名称将与类名相同。例如,JDBCHelperService 类的属性名称将为 JDBCHelperService,而不是 jDBCHelperServicejdbcHelperService

请参见 JavaBean 规范的第 8.8 节,以获取有关取消首字母大写的规则的更多信息。

只有顶级对象才能注入,因为遍历所有嵌套对象以执行注入将是一个性能问题。

在注入非默认数据源时请务必小心。例如,使用此配置

dataSources:
    dataSource:
        pooled: true
        jmxExport: true
        .....
    secondary:
        pooled: true
        jmxExport: true
        .....

你可以像预期的那样注入主 dataSource

class BookSqlService {

      def dataSource
}

但是要注入 secondary 数据源,你必须使用 Spring 的 Autowired 注入或 resources.groovy

class BookSqlSecondaryService {

  @Autowired
  @Qualifier('dataSource_secondary')
  def dataSource2
}

依赖注入和服务

你可以使用相同的技术在其他服务中注入服务。如果你有一个需要使用 BookServiceAuthorService,则如下声明 AuthorService 将允许这样做

class AuthorService {
    def bookService
}

依赖注入和域类/标签库

你甚至可以将服务注入到域类和标签库中,这有助于开发丰富的域模型和视图

class Book {
    ...
    def bookService

    def buyBook() {
        bookService.buyBook(this)
    }
}
自 Grails 3.2.8 之后,此功能在默认情况下并未启用。如果您想再次启用此功能,请参阅 Spring 对域实例的自动装配

服务 Bean 名称

如果在不同的包中定义了多个同名服务,与服务关联的默认 Bean 名称可能会存在问题。例如,考虑以下情况:一个应用程序定义了一个名为 com.demo.ReportingService 的服务类,并且该应用程序使用了名为 ReportingUtilities 的插件,而该插件提供了一个名为 com.reporting.util.ReportingService 的服务类。

这两个类的默认 Bean 名称都将为 reportingService,因此它们会相互冲突。Grails 通过在 Bean 名称前加上插件名称的方式来更改由插件提供的服务的默认 Bean 名称,从而解决此问题。

在上面描述的场景中,reportingService Bean 将是应用程序中定义的 com.demo.ReportingService 类的实例,reportingUtilitiesReportingService Bean 将是 ReportingUtilities 插件提供的 com.reporting.util.ReportingService 类的实例。

对于插件提供的所有服务 Bean,如果应用程序或应用程序中的其他插件中没有其他同名服务,那么将创建一个不包含插件名称的 Bean 别名,并且该别名指向名称中包含插件名称前缀的 Bean。

例如,如果 ReportingUtilities 插件提供了一个名为 com.reporting.util.AuthorService 的服务,并且应用程序或应用程序使用的任何插件中都没有其他 AuthorService,那么将会有一个名为 reportingUtilitiesAuthorService 的 Bean,它是此 com.reporting.util.AuthorService 类的实例,并且上下文中将定义一个名为 authorService 的 Bean 别名,指向同一个 Bean。