(Quick Reference)

8 The Service Layer - Reference Documentation

Authors: Graeme Rocher, Peter Ledbrook, Marc Palmer, Jeff Brown, Luke Daley, Burt Beckwith

Version: null

8 The Service Layer

Grails中也有service层的概念. Grails团队不鼓励在controller中嵌入核心应用逻辑,因为这样不利于代码的重用,也影响清晰的分层。

Grails defines the notion of a service layer. The Grails team discourages the embedding of core application logic inside controllers, as it does not promote reuse and a clean separation of concerns.

Grails中,应用的主要逻辑都放在的service层,controller负责处理请求流程。

Services in Grails are the place to put the majority of the logic in your application, leaving controllers responsible for handling request flow with redirects and so on.

创建一个Service

Creating a Service

要创建一个Grails service,你只要进入命令行模式,在项目的根目录下,执行create-service命令:

You can create a Grails service by running the create-service command from the root of your project in a terminal window:

grails create-service helloworld.simple

如果create-service脚本中没有指定package,Grails会自动使用程序的名称为package的名称。

If no package is specified with the create-service script, Grails automatically uses the application name as the package name.

这样就会创建一个service,这个service位于grails-app/services/helloworld/SimpleService.groovy .除了名字按照Grails的约定以Service结尾以外,这个文件就是一个普通的Groovy类:

The above example will create a service at the location grails-app/services/helloworld/SimpleService.groovy. A service's name ends with the convention Service, other than that a service is a plain Groovy class:

package helloworld

class SimpleService { }

8.1 Declarative Transactions

h3. Default Declarative Transactions

声明式事务

Services are typically involved with coordinating logic between domain classes, and hence often involved with persistence that spans large operations. Given the nature of services, they frequently require transactional behaviour. You can use programmatic transactions with the withTransaction method, however this is repetitive and doesn't fully leverage the power of Spring's underlying transaction abstraction.

Services通常会包含这样的逻辑--需要多个domain类之间相互配合。因此它常常会出现这样的情况:涉及到的持久化包括大量的数据库操作。这些问题使得service中经常都需要对方法进行事务管理。当然你可以用withTransaction 方法来管理事务,但是这样很繁琐,也不能充分利用Spring的强大的事务抽象能力。

Grails中可以对service进行事务划分,它声明service中所有方法都是事务型的。缺省所有的service都进行了事务划分。要禁用这个配置,只需要设置transactional 属性为false:

Services enable transaction demarcation, which is a declarative way of defining which methods are to be made transactional. All services are transactional by default. To disable this set the transactional property to false:

class CountryService {
    static transactional = false
}

你也可以设置这个属性为true,以防止将来这个默认值改变后对你的应用造成影响,或者是明确声明service是事务型的。

You may also set this property to true to make it clear that the service is intentionally transactional.

警告: 依赖注入是使声明式事务工作的唯一途径。如果你自己用new操作符,比如new BookService(),将不能得到一个事务型的service.

Warning: dependency injection is the only way that declarative transactions work. You will not get a transactional service if you use the new operator such as new BookService()

这样的结果是所有的方法被包装在一个事务中,在方法中有抛出Runtime 异常Error时,将会自动回滚。事务传播级别默认是PROPAGATION_REQUIRED.

Checked异常不会回滚事务. Groovy认为checked和unchecked异常非常相似,但Spring不知道这个道理并且使用默认值. 因此必须有了解checked和unchecked异常之间的差异。

The result is that all methods are wrapped in a transaction and automatic rollback occurs if a method throws a runtime exception (ie one that extends RuntimeException) or an Error. The propagation level of the transaction is by default set to PROPAGATION_REQUIRED .

Checked exceptions do not roll back transactions. Even though Groovy blurs the distinction between checked and unchecked exceptions, Spring isn't aware of this and its default behaviour is used, so it's important to understand the distinction between checked and unchecked exceptions.

Custom Transaction Configuration

Grails also fully supports Spring's Transactional annotation for cases where you need more fine-grained control over transactions at a per-method level or need specify an alternative propagation level.

Annotating a service method with Transactional disables the default Grails transactional behavior for that service (in the same way that adding transactional=false does) so if you use any annotations you must annotate all methods that require transactions.

In this example listBooks uses a rea​​d-only transaction, updateBook uses a default read-write transaction, and deleteBook is not transactional (probably not a good idea given its name).

自定事务配置

当你需要更细粒度的交易控制或需要指定另类传播级别的时候,Grails也支持Spring的 Transactional注释。

使用 Transactional注释会停用Grails对该Service的默认行为。所以如果你使用任何注释,你必须注解的所有方法

在这个例子中listBooks只使用只读的事务,updateBook使用一个读写事务,deleteBook不是事务性的(看它的名称这可能不是一个好主意)。

import org.springframework.transaction.annotation.Transactional

class BookService {

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

@Transactional def updateBook() { // … }

def deleteBook() { // … } }

您也可以注解全程Service的交易行为然后重写每个方法的交易行为。这项Service是相当于一个没有注释的Service(因为默认值总是 Transactional= TRUE):

You can also annotate the class to define the default transaction behavior for the whole service, and then override that default per-method. For example, this service is equivalent to one that has no annotations (since the default is implicitly transactional=true ):

import org.springframework.transaction.annotation.Transactional

@Transactional class BookService {

def listBooks() { Book.list() }

def updateBook() { // … }

def deleteBook() { // … } }

为这个例子中类级别的注释保证所有方法为读写事务,但 listBooks方法重写为只读的交易:

This version defaults to all methods being read-write transactional (due to the class-level annotation), but the listBooks method overrides this to use a rea​​d-only transaction:

import org.springframework.transaction.annotation.Transactional

@Transactional class BookService {

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

def updateBook() { // … }

def deleteBook() { // … } }

虽然在这个例子中 updateBook deleteBook没有注明注释,它们继承了类级别的注释配置。

如需详细资讯,请参阅Spring的用户指南Using @Transactional.

Grails和Spring之间不同的特点是Grails使用Transactional时不需要任何先前的配置。

Although updateBook and deleteBook aren't annotated in this example, they inherit the configuration from the class-level annotation.

For more information refer to the section of the Spring user guide on Using @Transactional.

Unlike Spring you do not need any prior configuration to use Transactional; just specify the annotation as needed and Grails will detect them up automatically.

8.1.1 Transactions Rollback and the Session

Understanding Transactions and the Hibernate Session

When using transactions there are important considerations you must take into account with regards to how the underlying persistence session is handled by Hibernate. When a transaction is rolled back the Hibernate session used by GORM is cleared. This means any objects within the session become detached and accessing uninitialized lazy-loaded collections will lead to LazyInitializationExceptions.

To understand why it is important that the Hibernate session is cleared. Consider the following example:

class Author {
    String name
    Integer age

static hasMany = [books: Book] }

If you were to save two authors using consecutive transactions as follows:

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

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

Only the second author would be saved since the first transaction rolls back the author save() by clearing the Hibernate session. If the Hibernate session were not cleared then both author instances would be persisted and it would lead to very unexpected results.

It can, however, be frustrating to get LazyInitializationExceptions due to the session being cleared.

For example, consider the following example:

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}" } } }

In the above example the transaction will be rolled back if the Author's age exceeds the maximum value defined in the isTooOld() method by throwing an AuthorException. The AuthorException references the author but when the books association is accessed a LazyInitializationException will be thrown because the underlying Hibernate session has been cleared.

To solve this problem you have a number of options. One is to ensure you query eagerly to get the data you will need:

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

In this example the books association will be queried when retrieving the Author.

This is the optimal solution as it requires fewer queries then the following suggested solutions.

Another solution is to redirect the request after a transaction rollback:

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

In this case a new request will deal with retrieving the Author again. And, finally a third solution is to retrieve the data for the Author again to make sure the session remains in the correct state:

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}" } } }

Validation Errors and Rollback

A common use case is to rollback a transaction if there are validation errors. For example consider this service:

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) } } }

To re-render the same view that a transaction was rolled back in you can re-associate the errors with a refreshed instance before rendering:

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] } } }

8.2 Scoped Services

By default, access to service methods is not synchronised, so nothing prevents concurrent execution of those methods. In fact, because the service is a singleton and may be used concurrently, you should be very careful about storing state in a service. Or take the easy (and better) road and never store state in a service.

You can change this behaviour by placing a service in a particular scope. The supported scopes are:

  • prototype - A new service is created every time it is injected into another class
  • request - A new service will be created per request
  • flash - A new service will be created for the current and next request only
  • flow - In web flows the service will exist for the scope of the flow
  • conversation - In web flows the service will exist for the scope of the conversation. ie a root flow and its sub flows
  • session - A service is created for the scope of a user session
  • singleton (default) - Only one instance of the service ever exists

If your service is flash, flow or conversation scoped it must implement java.io.Serializable and can only be used in the context of a Web Flow

To enable one of the scopes, add a static scope property to your class whose value is one of the above, for example

static scope = "flow"

8.3 Dependency Injection and Services

Dependency Injection Basics

A key aspect of Grails services is the ability to use Spring Framework's dependency injection features. Grails supports "dependency injection by convention". In other words, you can use the property name representation of the class name of a service to automatically inject them into controllers, tag libraries, and so on.

As an example, given a service called BookService, if you define a property called bookService in a controller as follows:

class BookController {
    def bookService
    …
}

In this case, the Spring container will automatically inject an instance of that service based on its configured scope. All dependency injection is done by name. You can also specify the type as follows:

class AuthorService {
    BookService bookService
}

NOTE: Normally the property name is generated by lower casing the first letter of the type. For example, an instance of the BookService class would map to a property named bookService.

To be consistent with standard JavaBean conventions, if the first 2 letters of the class name are upper case, the property name is the same as the class name. For example, the property name of the JDBCHelperService class would be JDBCHelperService, not jDBCHelperService or jdbcHelperService.

See section 8.8 of the JavaBean specification for more information on de-capitalization rules.

Dependency Injection and Services

You can inject services in other services with the same technique. If you had an AuthorService that needed to use the BookService, declaring the AuthorService as follows would allow that:

class AuthorService {
    def bookService
}

Dependency Injection and Domain Classes / Tag Libraries

You can even inject services into domain classes and tag libraries, which can aid in the development of rich domain models and views:

class Book {
    …
    def bookService

def buyBook() { bookService.buyBook(this) } }

8.4 Using Services from Java

One of the powerful things about services is that since they encapsulate re-usable logic, you can use them from other classes, including Java classes. There are a couple of ways you can reuse a service from Java. The simplest way is to move your service into a package within the grails-app/services directory. The reason this is important is that it is not possible to import classes into Java from the default package (the package used when no package declaration is present). So for example the BookService below cannot be used from Java as it stands:

class BookService {
    void buyBook(Book book) {
        // logic
    }
}

However, this can be rectified by placing this class in a package, by moving the class into a sub directory such as grails-app/services/bookstore and then modifying the package declaration:

package bookstore

class BookService { void buyBook(Book book) { // logic } }

An alternative to packages is to instead have an interface within a package that the service implements:

package bookstore

interface BookStore { void buyBook(Book book) }

And then the service:

class BookService implements bookstore.BookStore {
    void buyBook(Book b) {
        // logic
    }
}

This latter technique is arguably cleaner, as the Java side only has a reference to the interface and not to the implementation class (although it's always a good idea to use packages). Either way, the goal of this exercise to enable Java to statically resolve the class (or interface) to use, at compile time.

Now that this is done you can create a Java class within the src/java directory and add a setter that uses the type and the name of the bean in Spring:

// src/java/bookstore/BookConsumer.java
package bookstore;

public class BookConsumer {

private BookStore store;

public void setBookStore(BookStore storeInstance) { this.store = storeInstance; } … }

Once this is done you can configure the Java class as a Spring bean in grails-app/conf/spring/resources.xml (for more information see the section on Grails and Spring):

<bean id="bookConsumer" class="bookstore.BookConsumer">
    <property name="bookStore" ref="bookService" />
</bean>

or in grails-app/conf/spring/resources.groovy:

import bookstore.BookConsumer

beans = { bookConsumer(BookConsumer) { bookStore = ref("bookService") } }