Gangmax Blog

自由之思想,独立之精神

Grails for REST API

| Comments

In the recent several days I was working on a Grails web application which provides a REST API implementation.

1
2
3
4
5
# My environment information
master ~/code/apidoc> grails -version
| Grails Version: 3.2.4
| Groovy Version: 2.4.7
| JVM Version: 1.8.0_121

Basic

Scaffolding

Like Rails, you can create the project structure by executing some commands easily. In my case I used the following ones:

1
2
3
4
5
6
7
grails create-app apidoc --profile=rest-api
grails create-domain-class me.gangmax.apidoc.Project
grails generate-all me.gangmax.apidoc.Project
grails run-app
grails create-domain-class me.gangmax.apidoc.Feature
grails generate-all me.gangmax.apidoc.Feature
grails run-app

Note that based on the ”profile” concept in Grails, I can just generate the files for REST API instead of the files needed for an ordinary HTML view web application. This is done by using the “grails create-app apidoc –profile=rest-api” command above.

Now a running web application is ready although no logic is added yet.

Database

Grails use “H2” database by default. If you want to switch to such as MySQL, you can refer here.

Before deploying your Grails application to production environment, you need to generate DDL script and create the database/tables in your RDBMS such as MySQL. Refer here to view how to do it. Run commands like below:

1
2
3
4
# By default the "development" environment is used.
grails schema-export
# Specify the environment to be used.
grails prod schema-export

Package & Deployment

There are several ways to deploy a Grails application. Refer here to get the details.

In my case, I choose the “runnable war” manner:

1
2
3
4
5
6
7
8
9
10
11
# Package for specific environment. The war file is generated in "build/libs/".
grails prod package
# By default the "production" environment is used. So this line has the
# same result as the above line.
grails package
# Put the war file to any server having Java 1.8. Run the following command
# to start it. Note the default port of Grails is "8080", you can change that
# by using the "--server.port" argument in the command line below:
java -jar build/libs/apidoc-0.1.war --server.port=8000
# To run the Grails web application as background process, you can run:
nohup java -jar apidoc-0.1.war > /dev/null 2>&1 &

Problems

Time format

In the JSON output of the Grials REST API, by default the “java.util.Date” type will be rendered into the string format like “2017-01-22T17:23:17Z”. It took me a lot of time to figure out how to customize this time string into some other format like “2017-01-22 17:23:17+0800”.

First, the following solution does not work for me:

1
2
http://stackoverflow.com/questions/690370/how-to-return-specific-date-format-as-json-in-grails
http://stackoverflow.com/questions/14706293/how-to-set-date-format-for-json-converter-in-grails

Finally I figure out the following solution by myself:

Update the “apidoc/grails-app/views/*/.json” files to customize the JSON output, for me the files are:

1
2
apidoc/grails-app/views/feature/_feature.gson
apidoc/grails-app/views/project/_project.gson

Note that the “*.gson” files are also valid Groovy script:

_project.gson
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/*
 The view here is used to generate the JSON output for the domain class. Especially for the "Date" type
 to override the default "2017-01-20T14:50:37Z" time format string.

 The solution comes from: http://views.grails.org/latest/#_templates

 I tried the following solution first but it did not work:
 "How to set date format for JSON converter in Grails?"
  http://stackoverflow.com/questions/14706293/how-to-set-date-format-for-json-converter-in-grails
  http://stackoverflow.com/questions/690370/how-to-return-specific-date-format-as-json-in-grails/694845#694845
 */

import com.jd.xdata.apidoc.Project

model {
  Project project
}

//json g.render(project)

json {
    id project.id
    name project.name
    features project.features ? g.render(template:"/feature/feature", collection: project.features, var:'feature') : null
    createdTime project.createdTime.format("yyyy-MM-dd HH:mm:ssZ")
    updatedTime project.updatedTime.format("yyyy-MM-dd HH:mm:ssZ")
}
_feature.gson
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import com.jd.xdata.apidoc.Feature

model {
  Feature feature
}

//json g.render(feature)

json {
    id feature.id
    project feature.project.id
    endpoint feature.endpoint
    httpMethod feature.httpMethod
    description feature.description
    requestBody feature.requestBody
    responseBody feature.responseBody
    note feature.note
    createdTime feature.createdTime.format("yyyy-MM-dd HH:mm:ssZ")
    updatedTime feature.updatedTime.format("yyyy-MM-dd HH:mm:ssZ")
}

Standalone war running exception

For the first time I run the Grails web application in the ”standalone war” mode, I got the following exception:

1
2
3
4
5
Caused by: java.io.FileNotFoundException: JAR entry !/META-INF/services/org.hibernate.boot.registry.selector.StrategyRegistrationProvider not found in C:\Users\frank\AppData\Local\Temp\jar_cache3915609090913132890.tmp
at sun.net.www.protocol.jar.JarURLConnection.connect(Unknown Source)
at sun.net.www.protocol.jar.JarURLConnection.getInputStream(Unknown Source)
at java.net.URL.openStream(Unknown Source)
... 95 common frames omitted

The solution is here: add the following content at the end of the “build.gradle” file:

1
2
3
ext {
    set "tomcat.version", "8.5.5"
}

MySQL Chinese encoding error

The problem need a 2-part solution:

  1. In the MySQL server side
1
2
3
4
5
6
7
8
9
10
11
12
-- From:
-- http://stackoverflow.com/a/24269542/3115617
-- http://www.pc6.com/infoview/Article_63586.html
-- When creating the database, set the text encoding items correctly.
drop database `apidoc`;
CREATE DATABASE `apidoc` CHARACTER SET 'utf8' COLLATE 'utf8_general_ci';
show variables like "%char%";
SET character_set_client='utf8';
SET character_set_connection='utf8';
SET character_set_results='utf8';
show variables like "%char%";
-- Then create the tables(omit).
  1. In the Grails application side
1
2
# In the "grails-app/conf/application.yml" file, update the database "datasource" info:
url: jdbc:mysql://your.mysql.server:3306/apidoc?useUnicode=true&characterEncoding=UTF-8

Grails logging

To add logging feature to your Grails application, you need to update the “grails-app/conf/logback.groovy” file in your project:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Configure the rolling file appender for the production environment logging.
appender('ROLLING_FILE', RollingFileAppender) {
    rollingPolicy(SizeAndTimeBasedRollingPolicy) {
        fileNamePattern = "${System.getProperty('user.dir')}/apidoc-%d{yyyy-MM-dd}.%i.log"
        maxFileSize = FileSize.valueOf('100MB')
        maxHistory = 60
        totalSizeCap = FileSize.valueOf('10GB')
    }
    encoder(PatternLayoutEncoder) {
        charset = Charset.forName('UTF-8')
        pattern = '%d{yyyy-MM-dd HH:mm:ss.SSS} ' + // Date
                    '%5p ' + // Log level
                    '---[%15.15t] ' + // Thread
                    '%-40.40logger{39}: ' + // Logger
                    '%m%n%wex' // Message
    }
}

...
}else if (Environment.current == Environment.PRODUCTION) {
    // Write to rolling log file when running in the production environment.
    root(INFO, ['STDOUT', 'ROLLING_FILE'])
}
...

The most important point of the “logback.groovy” configuration is the following sentence from here:

Most appenders require properties to be set and sub-components to be injected to function properly. Properties are set using the ‘=’ operator (assignment). Sub-components are injected by invoking a method named after the property and passing that method the class to instantiate as an argument. This convention can be applied recursively to configure properties as well as sub-components of any appender sub-component. This approach is at the heart of logback.groovy scripts and is probably the only convention that needs learning.

Another point is that, in your Grails controller code, a variable named “log” is avalaible without the need of declaration, which means you can use it to write logging lines directly.

Refer here, here and here for more details:

Comments