8.2 Spring Boot集成Groovy、Grails開發

8.2 Spring Boot集成Groovy、Grails開發

本章介紹Spring Boot集成Groovy,Grails開發。我們將開發一個極簡版的pms(項目管理系統)。

Groovy和Grails簡介

Groovy簡介

Groovy 是一種動態語言,它在 JVM 上運行,并且與 Java 語言無縫集成。

Groovy 可以大大減少 Java 代碼的數量。在 Groovy 中,不再需要為字段編寫 getter 和 setter 方法,因為 Groovy 會自動提供它們。不再需要編寫 for Iterator i = list.iterator() 來循環遍歷一系列的項;list.each 可以做相同的事情,而且看上去更簡潔,表達更清晰。簡言之,Groovy 就是 21 世紀的 Java 語言。[2]

Groovy 不會替代 Java 語言 — 它只是提供了增強。您可以很快地掌握 Groovy,因為說到底,Groovy 代碼就是 Java 代碼。這兩種語言是如此兼容,甚至可以將一個 .java 文件重命名為一個 .groovy 文件 — 例如,將 Person.java 改為 Person.groovy — 從而得到一個有效的(可執行的)Groovy 文件(雖然這個 Groovy 文件并沒有用到 Groovy 提供的任何語法)。

Grails簡介

Grails是一套用于快速Web應用開發的開源框架,它基于Groovy編程語言,并構建于Spring、Hibernate等開源框架之上,是一個高生產力一站式框架。

Grails這個獨特的框架被視為是提升工程師生產效率的動態工具,因為其干脆的API設計,合理的默認值以及約定架構。與java的無縫集成使得這個框架成為世界上眾多框架中的首選。一系列強大的特性,如基于sping的依賴注入和各式各樣的插件,可以提供創建現代基于web的app的所有需要的東西。

我們使用Grails框架。就像 Rails 與 Ruby 編程語言聯系非常緊密一樣,Grails 也離不開 Groovy。

DRY(Don't Repeat Yourself,不要重復自己)
約定優于配置(Convention over Configuration)

DRY和約定優先于配置的思想,是由Rails興起并迅速被廣泛接收和欣賞的Web框架新思路。Grails作為JEE世界的Rails,把這些最前沿的設計理念帶入已顯得陳舊的JEE社區,擁有鮮明突出的特點,以及由此帶來的優秀的開發效率。

對Grails來說,Groovy是其能夠實現靈活多變的快速開發,區別于其他運行于JVM之上的Web框架的核心技術。

Groovy的動態特性是其最大亮點,在這方面幾乎不輸于Ruby等其他熱門的動態語言。[3]

Grails實現原理

  • 基于Spring MVC的控制器層

  • 構建于Gant 上的命令行腳本運行環境,內置Tomcat服務器,不用重新啟動服務器就可以進行重新加載

  • 基于Spring的MessageSource核心概念,提供了對國際化(i18n)的支持

  • 基于Spring事務抽象概念,實現事務服務層[1]

Github:https://github.com/grails
官網:https://grails.org/

數據庫的對象關系映射層使用GORM

我們使用 Grail 對象關系映射(Grails Object Relational Mapping,GORM)API 進行數據庫層的持久化工作。

安裝Grails 3 開發環境

瀏覽器訪問 http://www.grails.org/Download,下載,解壓,設置環境變量即可。具體步驟如下:

1.下載并解壓 grails.zip。
2.創建一個 GRAILS_HOME 環境變量。
3.將 $GRAILS_HOME/bin 添加到 PATH中。

如果你的電腦上有SDKMAN! (The Software Development Kit Manager),可以直接命令行自動安裝Grails最新穩定版本:

$ sdk install grails

安裝完畢,驗證一下:

$ grails -v

| Grails Version: 3.2.8
| Groovy Version: 2.4.10
| JVM Version: 1.8.0_40

OK, grails開發環境搞定。我們可以看到,grails依賴的Groovy,JVM環境版本。

創建Grails項目

讓我們來體驗JVM上的Ruby on rails式的命令行自動工程生成的快感吧!

命令行直接運行:

$ grails create-app pms
Resolving dependencies..
| Application created at /Users/jack/book/pms

我們就生成了一個grails工程demo,目錄如下:

.
├── build.gradle
├── gradle
│   └── wrapper
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── grails-app
│   ├── assets
│   │   ├── images
│   │   │   ├── apple-touch-icon-retina.png
│   │   │   ├── apple-touch-icon.png
│   │   │   ├── favicon.ico
│   │   │   ├── grails-cupsonly-logo-white.svg
│   │   │   ├── skin
│   │   │   │   ├── database_add.png
│   │   │   │   ├── database_delete.png
│   │   │   │   ├── database_edit.png
│   │   │   │   ├── database_save.png
│   │   │   │   ├── database_table.png
│   │   │   │   ├── exclamation.png
│   │   │   │   ├── house.png
│   │   │   │   ├── information.png
│   │   │   │   ├── shadow.jpg
│   │   │   │   ├── sorted_asc.gif
│   │   │   │   └── sorted_desc.gif
│   │   │   └── spinner.gif
│   │   ├── javascripts
│   │   │   ├── application.js
│   │   │   ├── bootstrap.js
│   │   │   └── jquery-2.2.0.min.js
│   │   └── stylesheets
│   │       ├── application.css
│   │       ├── bootstrap.css
│   │       ├── errors.css
│   │       ├── grails.css
│   │       ├── main.css
│   │       └── mobile.css
│   ├── conf
│   │   ├── application.yml
│   │   ├── logback.groovy
│   │   └── spring
│   │       └── resources.groovy
│   ├── controllers
│   │   └── pms
│   │       └── UrlMappings.groovy
│   ├── domain
│   ├── i18n
│   │   ├── messages.properties
│   │   ├── messages_cs_CZ.properties
│   │   ├── messages_da.properties
│   │   ├── messages_de.properties
│   │   ├── messages_es.properties
│   │   ├── messages_fr.properties
│   │   ├── messages_it.properties
│   │   ├── messages_ja.properties
│   │   ├── messages_nb.properties
│   │   ├── messages_nl.properties
│   │   ├── messages_pl.properties
│   │   ├── messages_pt_BR.properties
│   │   ├── messages_pt_PT.properties
│   │   ├── messages_ru.properties
│   │   ├── messages_sv.properties
│   │   ├── messages_th.properties
│   │   └── messages_zh_CN.properties
│   ├── init
│   │   └── pms
│   │       ├── Application.groovy
│   │       └── BootStrap.groovy
│   ├── services
│   ├── taglib
│   ├── utils
│   └── views
│       ├── error.gsp
│       ├── index.gsp
│       ├── layouts
│       │   └── main.gsp
│       └── notFound.gsp
├── grails-wrapper.jar
├── grailsw
├── grailsw.bat
├── settings.gradle
└── src
    ├── integration-test
    │   └── groovy
    ├── main
    │   ├── groovy
    │   └── webapp
    └── test
        └── groovy

29 directories, 62 files

這真的是一鍵生成。

我們可以直接使用下面的命令運行這個工程:

$ grails run-app

它會自動下載gradle-3.4.1-bin.zip(通常會很慢):

| Resolving Dependencies. Please wait...
Downloading https://services.gradle.org/distributions/gradle-3.4.1-bin.zip

如果我們本地有gradle環境,我們也可將此工程導入idea,配置一下本地的gradle環境。如下圖所示:

首次構建,gradle需要下載工程依賴的jar包。

我們可以看到build.gradle里面已經配置好一切:

buildscript {
    repositories {
        mavenLocal()
        maven { url "https://repo.grails.org/grails/core" }
    }
    dependencies {
        classpath "org.grails:grails-gradle-plugin:$grailsVersion"
        classpath "com.bertramlabs.plugins:asset-pipeline-gradle:2.14.1"
        classpath "org.grails.plugins:hibernate5:${gormVersion-".RELEASE"}"
    }
}

version "0.1"
group "pms"

apply plugin:"eclipse"
apply plugin:"idea"
apply plugin:"war"
apply plugin:"org.grails.grails-web"
apply plugin:"org.grails.grails-gsp"
apply plugin:"asset-pipeline"

repositories {
    mavenLocal()
    maven { url "https://repo.grails.org/grails/core" }
}

dependencies {
    compile "org.springframework.boot:spring-boot-starter-logging"
    compile "org.springframework.boot:spring-boot-autoconfigure"
    compile "org.grails:grails-core"
    compile "org.springframework.boot:spring-boot-starter-actuator"
    compile "org.springframework.boot:spring-boot-starter-tomcat"
    compile "org.grails:grails-dependencies"
    compile "org.grails:grails-web-boot"
    compile "org.grails.plugins:cache"
    compile "org.grails.plugins:scaffolding"
    compile "org.grails.plugins:hibernate5"
    compile "org.hibernate:hibernate-core:5.1.3.Final"
    compile "org.hibernate:hibernate-ehcache:5.1.3.Final"
    console "org.grails:grails-console"
    profile "org.grails.profiles:web"
    runtime "com.bertramlabs.plugins:asset-pipeline-grails:2.14.1"
    runtime "com.h2database:h2"
    testCompile "org.grails:grails-plugin-testing"
    testCompile "org.grails.plugins:geb"
    testRuntime "org.seleniumhq.selenium:selenium-htmlunit-driver:2.47.1"
    testRuntime "net.sourceforge.htmlunit:htmlunit:2.18"
}

bootRun {
    jvmArgs('-Dspring.output.ansi.enabled=always')
    addResources = true
}


assets {
    minifyJs = true
    minifyCss = true
}

我們在application.yml里面配置一下server.port (默認8080):

server:
    port: 8008

命令行執行(我們也可以使用grails run-app運行工程,區別是grails會下載外部gradle包,配置的gradle環境不是本地機器):

gradle bootRun

你將看到類似如下啟動日志:

02:18:02: Executing external task 'bootRun'...
:compileJava NO-SOURCE
:compileGroovy UP-TO-DATE
:buildProperties UP-TO-DATE
:processResources
:classes
:findMainClass
objc[27257]: Class JavaLaunchHelper is implemented in both /Library/Java/JavaVirtualMachines/jdk1.8.0_40.jdk/Contents/Home/bin/java (0x1001e44c0) and /Library/Java/JavaVirtualMachines/jdk1.8.0_40.jdk/Contents/Home/jre/lib/libinstrument.dylib (0x101a4e4e0). One of the two will be used. Which one is undefined.
:bootRun
Grails application running at http://localhost:8008 in environment: development

啟動完畢,訪問http://localhost:8008/,你將看到如下頁面:

螢幕快照 2017-04-15 02.10.49.png

為了演示上的簡易性,數據庫我們直接用的是H2,在application.yml配置如下:

hibernate:
    cache:
        queries: false
        use_second_level_cache: true
        use_query_cache: false
        region.factory_class: org.hibernate.cache.ehcache.SingletonEhCacheRegionFactory

dataSource:
    pooled: true
    jmxExport: true
    driverClassName: org.h2.Driver
    username: sa
    password:

environments:
    development:
        dataSource:
            dbCreate: create-drop
            url: jdbc:h2:mem:devDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE
    test:
        dataSource:
            dbCreate: update
            url: jdbc:h2:mem:testDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE
    production:
        dataSource:
            dbCreate: none
            url: jdbc:h2:./prodDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE
            properties:
                jmxEnabled: true
                initialSize: 5
                maxActive: 50
                minIdle: 5
                maxIdle: 25
                maxWait: 10000
                maxAge: 600000
                timeBetweenEvictionRunsMillis: 5000
                minEvictableIdleTimeMillis: 60000
                validationQuery: SELECT 1
                validationQueryTimeout: 3
                validationInterval: 15000
                testOnBorrow: true
                testWhileIdle: true
                testOnReturn: false
                jdbcInterceptors: ConnectionState
                defaultTransactionIsolation: 2 # TRANSACTION_READ_COMMITTED

建立領域模型Model層

我們繼續在當前工程根目錄下。使用grails的create-domain-class命令創建領域類:

$grails create-domain-class Project

執行這個命令,grails也會下gradle包。下載完,grails程序自動解壓,放到約定的目錄,日志如下:


| Resolving Dependencies. Please wait...
Downloading https://services.gradle.org/distributions/gradle-3.4.1-bin.zip
.
.
.

Unzipping /Users/jack/.gradle/wrapper/dists/gradle-3.4.1-bin/71zneekfcxxu7l9p7nr2sc65s/gradle-3.4.1-bin.zip to /Users/jack/.gradle/wrapper/dists/gradle-3.4.1-bin/71zneekfcxxu7l9p7nr2sc65s
Set executable permissions for: /Users/jack/.gradle/wrapper/dists/gradle-3.4.1-bin/71zneekfcxxu7l9p7nr2sc65s/gradle-3.4.1/bin/gradle
Cleaned up directory '/Users/jack/book/pms/build/classes/main'
Cleaned up directory '/Users/jack/book/pms/build/resources/main'

CONFIGURE SUCCESSFUL

Total time: 2 mins 43.589 secs
| Created grails-app/domain/pms/Project.groovy
| Created src/test/groovy/pms/ProjectSpec.groovy

我們繼續創建項目Project的里程碑Milestone領域類:

$ grails create-domain-class Milestone
| Created grails-app/domain/pms/Milestone.groovy
| Created src/test/groovy/pms/MilestoneSpec.groovy

我們可以看到這兩個類的代碼如下:

package pms

class Project {

    static constraints = {
    }
}

package pms

class Milestone {

    static constraints = {
    }
}


一開始,沒有什么內容。其中,static constraints變量里面主要定義對應的實體類的約束條件。

下面我們來設計領域對象的屬性。

一個項目Project,我們極簡化處理,取幾個代表的屬性,比如:名稱,負責人,開始時間,結束時間,狀態等。

package pms

class Project {

    static constraints = {
    }

    Integer id
    String name
    String owner
    Date startDate
    Date endDate
    String status
}

通常,一個項目,會有多個里程碑,所以我們這里的里程碑表,多條記錄對應一個Project。里程碑屬性我們就取: 關聯的項目id,名稱,負責人,計劃時間,實際時間,狀態。

package pms

class Milestone {

    static constraints = {
    }

    Integer id
    Integer projectId
    String name
    String owner
    Date expectDate
    Date actualDate
    String status
    
}

使用grails腳手架自動生成Controller層,視圖View層代碼

grails的腳手架控制值相當簡易,簡單易用。我們可以使用

grails create-controller $DomainName  : 創建DomainName對應的空Controller

grails generate-controller $DomainName :創建DomainName對應的包含CRUD的Controller

grails generate-all $DomainName: 創建DomainName對應的包含CRUD的Controller,以及對應的視圖view模板代碼

下面我們就使用grails generate-all來創建Project,Milestone的Controller,以及視圖。

$ grails generate-all Project
| Rendered template Controller.groovy to destination grails-app/controllers/pms/ProjectController.groovy
| Rendered template Spec.groovy to destination src/test/groovy/pms/ProjectControllerSpec.groovy
| Scaffolding completed for grails-app/domain/pms/Project.groovy
| Rendered template create.gsp to destination grails-app/views/project/create.gsp
| Rendered template edit.gsp to destination grails-app/views/project/edit.gsp
| Rendered template index.gsp to destination grails-app/views/project/index.gsp
| Rendered template show.gsp to destination grails-app/views/project/show.gsp
| Views generated for grails-app/domain/pms/Project.groovy



$ grails generate-all Milestone
| Rendered template Controller.groovy to destination grails-app/controllers/pms/MilestoneController.groovy
| Rendered template Spec.groovy to destination src/test/groovy/pms/MilestoneControllerSpec.groovy
| Scaffolding completed for grails-app/domain/pms/Milestone.groovy
| Rendered template create.gsp to destination grails-app/views/milestone/create.gsp
| Rendered template edit.gsp to destination grails-app/views/milestone/edit.gsp
| Rendered template index.gsp to destination grails-app/views/milestone/index.gsp
| Rendered template show.gsp to destination grails-app/views/milestone/show.gsp
| Views generated for grails-app/domain/pms/Milestone.groovy


下面是創建之后的工程目錄:

我們可以看出,通過統一的約定,我們得到規整的目錄結構。很好的體現了“約定優于配置(Convention over Configuration)”的方法論思想。

對控制器的理解可以歸結為三個 R:return、redirect 和 render。有些動作利用隱式的 return 語句將數據返回到具有相同名稱的 GSP 頁面。有些動作進行重定向。

我們看一下ProjectController的index方法:

    def index(Integer max) {
        params.max = Math.min(max ?: 10, 100)
        respond Project.list(params), model:[projectCount: Project.count()]
    }

我們沒有寫list,count,hasErros等方法,GROM都幫我們打理好一切了。具體的實現源碼在org.grails.datastore.gorm里面。這個處理方案跟Spring-jpa的思想基本是一樣的。都是通過注解元編程,動態生成相應的方法代碼。

部署測試

完成上述步驟,我們就已經有了包含CRUD基本功能的Web應用了,使用

gradle bootRun

命令運行工程,使用瀏覽器訪問:http://localhost:8008/
你將看到如下頁面:

我們可以看到,“Available Controllers”列表,這個功能模塊是通過如下一段gsp代碼實現的:

            <div id="controllers" role="navigation">
                <h2>Available Controllers:</h2>
                <ul>
                    <g:each var="c" in="${grailsApplication.controllerClasses.sort { it.fullName } }">
                        <li class="controller">
                            <g:link controller="${c.logicalPropertyName}">${c.fullName}</g:link>
                        </li>
                    </g:each>
                </ul>
            </div>

新建一個Project,保存,如下圖:

點擊Project列表頁:

編輯該項目:

Grails通過UrlMappings統一Url映射,簡化了Controller到View的映射路徑的代碼。只要我們按照“約定”的目錄結構組織我們的代碼即可。

package pms

class UrlMappings {

    static mappings = {
        "/$controller/$action?/$id?(.$format)?"{
            constraints {
                // apply constraints here
            }
        }

        "/"(view:"/index")
        "500"(view:'/error')
        "404"(view:'/notFound')
    }
}

Grails框架里面充滿了大量“約定規則”,按照“約定規則”編程,我們看到了,代碼是如此之“極簡”。

我們簡單看一個例子。如下圖:

這里的“New Milestone”,是怎么實現的呢?我們來看一下milestone/index.gsp里面的一段代碼:

<g:message code="default.list.label" args="[entityName]" />

這里的default.list.label值配置在i18n/messages.properties里面。

default.home.label=Home
default.list.label={0} List
default.add.label=Add {0}
default.new.label=New {0}
default.create.label=Create {0}
default.show.label=Show {0}
default.edit.label=Edit {0}

default.button.create.label=Create
default.button.edit.label=Edit
default.button.update.label=Update
default.button.delete.label=Delete
default.button.delete.confirm.message=Are you sure?

不過,在這種.properties配置文件中,中文可讀性比較差。類似這樣子:

default.blank.message=[{1}]\u7C7B\u7684\u5C5E\u6027[{0}]\u4E0D\u80FD\u4E3A\u7A7A

gsp代碼中,以 g: 為前綴的就是 GroovyTag。

本章pms項目工程源碼:
https://github.com/EasySpringBoot/pms

小結

參考資料

1.http://baike.baidu.com/item/grails
2.https://www.ibm.com/developerworks/cn/java/j-grails01158/
3.http://www.infoq.com/cn/articles/case-study-grails-partii/

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,030評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,310評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,951評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,796評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,566評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,055評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,142評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,303評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,799評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,683評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,899評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,409評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,135評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,520評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,757評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,528評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,844評論 2 372

推薦閱讀更多精彩內容