文章目录
- Grails4.0.11
- 介绍
- 相关依赖
- 入门
- ⎮Java环境搭建
- ⎮Grails环境搭建
- ⎮如何创建grails应用
- ⎮如何选择开发工具
- IDEA如何导入Grails项目
- ⎮目录结构,约定优于配置
- ⎮如何部署Grails项目
- 1. 使用默认内嵌的Tomcat8部署
- 2. 使用外部Tomtcat8部署
- 3. 使用外部Tomcat7部署
- ⎮Grails4支持的JavaEE容器有哪些
- ⎮如何快速创建控制器和领域类
- 1. 创建领域类
- 2. 使用脚手架创建控制器和视图
- 3. 使用脚手架生成的代码为我们实现了哪些功能
- 3.1 - 书籍列表页面
- 3.2 - 添加书籍页面
- 3.3 - 书籍详情页面
- 3.4 - 修改书籍页面
- 3.5 - 删除书籍页面
- 3.6 - 小结
- ⎮完成入门
Grails4.0.11
介绍
Grails 是一个全栈框架,它建立在已存在的 Java 技术(如 Spring 和 Hibernate)之上。
Grails核心技术及其相关插件解决了很多 Web 开发难题,降低了在 Java 平台上构建 Web 应用程序的复杂性。
Grails提供了开箱即用的功能:
- GORM - 一个易于使用的对象映射库,支持 SQL、MongoDB、Neo4j 等。
- 查看用于呈现 HTML 和 JSON 的技术
- 基于 Spring Boot 构建的控制器层
- 一个包含数百个插件的插件系统
- 使用 AngularJS、React 等创建应用程序的灵活配置文件
- 基于 Gradle 的交互式命令行环境和构建系统
- 一个嵌入式 Tomcat 容器,配置为即时重新加载
相关依赖
Groovy | GORM | Hibernate | SpringFramework | SpringBoot | Gradle | Spock |
2.5.14 | 7 | 5.4 | 5.1.20 | 2.1.18 | 5.6.4 | 1.3 |
入门
⎮Java环境搭建
在安装 Grails 4.0.11 之前,至少需要安装JDK1.8版本才行,并且设置JAVA_HOME 的环境变量。
JAVA_HOME="/Library/Java/JavaVirtualMachines/jdk1.8.0_171.jdk/Contents/Home"
export PATH=".:$PATH:$JAVA_HOME/bin"
export JAVA_HOME
⎮Grails环境搭建
- 下载 Grails4.0.11 的二进制发行版并将生成的 zip 文件解压缩到/opt目录。
- 设置GRAILS_HOME环境变量,编辑~/.bash_profile
export GRAILS_HOME=/opt/grails-4.0.11
- 然后将 bin 目录添加到您的 PATH 变量中:
export PATH="$PATH:$GRAILS_HOME/bin"
- 打开新的命令行窗口,输入命令 grails -version查看输出内容
➜ /Users/xiaosan > grails -version
| Grails Version: 4.0.11
| JVM Version: 1.8.0_171
⎮如何创建grails应用
进入要创建项目的目录➜ /Users/xiaosan >cd Documents/work
使用create-app命令创建应用
➜ /Users/xiaosan/Documents/work >grails create-app helloworldgrails4
| Application created at /Users/xiaosan/Documents/work/helloworldgrails4
该命令会创建一个helloworldgrails4的项目目录,进入项目目录➜ /Users/xiaosan/Documents/work >cd helloworldgrails4
输入grails命令,启动 Grails 交互式控制台➜ /Users/xiaosan/Documents/work >grails
等待几秒会看到如下提示:
| Resolving Dependencies. Please wait...
| Starting interactive mode...
| Enter a command name to run. Use TAB for completion:
grails>
我们想要的是一个简单的页面,它只打印消息“Hello World!”到浏览器。在 Grails 中,无论何时您想要一个新页面,您只需为它创建一个新的控制器操作。由于我们还没有控制器,让我们现在使用 create-controller 命令创建一个:
grails> create-controller hello
| Created grails-app/controllers/helloworldgrails4/HelloController.groovy
| Created src/test/groovy/helloworldgrails4/HelloControllerSpec.groovy
创建hello时没有指定包名,默认以项目名称helloworldgrails4作为包名。
在输入命令时可以使用tab键,会自动补全命令或者提示命令。
查看HelloController.groovy文件:
package helloworldgrails4
class HelloController {
def index() { }
}
修改这个控制器,添加一个动作来生成“Hello World”页。
package helloworldgrails4
class HelloController {
def index() {
render "Hello World!"
}
}
动作只是一种方法。在这种特殊情况下,它调用 Grails 提供的特殊方法来呈现页面。
在grails交互控制台中启动项目:
grails> run-app
| Running application...
The Class-Path manifest attribute in /Users/xiaosan/.gradle/caches/modules-2/files-2.1/org.glassfish.jaxb/jaxb-runtime/2.3.1/dd6dda9da676a54c5b36ca2806ff95ee017d8738/jaxb-runtime-2.3.1.jar referenced one or more files that do not exist: file:/Users/xiaosan/.gradle/caches/modules-2/files-2.1/org.glassfish.jaxb/jaxb-runtime/2.3.1/dd6dda9da676a54c5b36ca2806ff95ee017d8738/jaxb-api-2.3.1.jar,file:/Users/xiaosan/.gradle/caches/modules-2/files-2.1/org.glassfish.jaxb/jaxb-runtime/2.3.1/dd6dda9da676a54c5b36ca2806ff95ee017d8738/txw2-2.3.1.jar,file:/Users/xiaosan/.gradle/caches/modules-2/files-2.1/org.glassfish.jaxb/jaxb-runtime/2.3.1/dd6dda9da676a54c5b36ca2806ff95ee017d8738/istack-commons-runtime-3.0.7.jar,file:/Users/xiaosan/.gradle/caches/modules-2/files-2.1/org.glassfish.jaxb/jaxb-runtime/2.3.1/dd6dda9da676a54c5b36ca2806ff95ee017d8738/stax-ex-1.8.jar,file:/Users/xiaosan/.gradle/caches/modules-2/files-2.1/org.glassfish.jaxb/jaxb-runtime/2.3.1/dd6dda9da676a54c5b36ca2806ff95ee017d8738/FastInfoset-1.2.15.jar,file:/Users/xiaosan/.gradle/caches/modules-2/files-2.1/org.glassfish.jaxb/jaxb-runtime/2.3.1/dd6dda9da676a54c5b36ca2806ff95ee017d8738/javax.activation-api-1.2.0.jar
The Class-Path manifest attribute in /Users/xiaosan/.gradle/caches/modules-2/files-2.1/com.sun.xml.bind/jaxb-impl/2.3.1/a1a12b85ba1435b4189e065f7dafcc3fb9410d38/jaxb-impl-2.3.1.jar referenced one or more files that do not exist: file:/Users/xiaosan/.gradle/caches/modules-2/files-2.1/com.sun.xml.bind/jaxb-impl/2.3.1/a1a12b85ba1435b4189e065f7dafcc3fb9410d38/jaxb-runtime-2.3.1.jar,file:/Users/xiaosan/.gradle/caches/modules-2/files-2.1/com.sun.xml.bind/jaxb-impl/2.3.1/a1a12b85ba1435b4189e065f7dafcc3fb9410d38/txw2-2.3.1.jar,file:/Users/xiaosan/.gradle/caches/modules-2/files-2.1/com.sun.xml.bind/jaxb-impl/2.3.1/a1a12b85ba1435b4189e065f7dafcc3fb9410d38/istack-commons-runtime-3.0.7.jar,file:/Users/xiaosan/.gradle/caches/modules-2/files-2.1/com.sun.xml.bind/jaxb-impl/2.3.1/a1a12b85ba1435b4189e065f7dafcc3fb9410d38/stax-ex-1.8.jar,file:/Users/xiaosan/.gradle/caches/modules-2/files-2.1/com.sun.xml.bind/jaxb-impl/2.3.1/a1a12b85ba1435b4189e065f7dafcc3fb9410d38/FastInfoset-1.2.15.jar,file:/Users/xiaosan/.gradle/caches/modules-2/files-2.1/com.sun.xml.bind/jaxb-impl/2.3.1/a1a12b85ba1435b4189e065f7dafcc3fb9410d38/javax.activation-api-1.2.0.jar
Grails application running at http://localhost:8080 in environment: development
<=======<==========---> 83% EXECUTING [2m 11s]
> :bootR> :bootRun
grails>
默认使用内嵌Tomcat8服务器,默认端口8080,在浏览器中输入http://localhost:8080/ 访问
这是Grails提供的介绍页面,页面位置在 grails-app/view/index.gsp。显示有多少个控制器,并提供指向它们的链接。
单击“HelloController”链接就可以看到包含文本“Hello World!”的自定义页面。
现在第一个 Grails 应用程序已经成功运行起来了。
为应用程序设置上下文路径,在 grails-app/conf/application.yml 中添加一个配置属性:
server:
servlet:
context-path: /helloworld
添加后的代码:
---
server:
servlet:
context-path: /helloworld
grails:
profile: web
codegen:
后面代码省略···
···
保存之后,系统会自动将项目运行在helloworld上,不需要重启
Grails application running at http://localhost:8080/helloworld in environment: development
<==========---> 83% EXECUTING [28m 43s]
> :bootRun
这个时候就不能使用http://localhost:8080访问项目了
要使用http://localhost:8080/helloworld访问项目
访问"HelloWorld"页面
注意:
- 控制器(Controller)可以包含许多动作(Action),每个动作对应一个不同的页面(此时忽略 AJAX)。
- 每个页面都可以通过一个唯一的 URL 访问。
- 该 URL 由控制器名称和操作名称组成:
/上下文路径/控制器/动作
/appname/controller/action。
这意味着您可以通过 /helloworld/hello/index 访问 Hello World 页面,其中
‘hello’ 是控制器名称(从类名中删除 ‘Controller’ 后缀和小写首字母),
‘index’ 是控制器动作名称。
因为“index”是默认操作,可以省略。例如
http://localhost:8080/helloworld/hello/index http://localhost:8080/helloworld/hello 都可以访问到HelloWorld页面
可以在启动项目时,通过命令参数指定上下文路径,这样就不用在applicatioin.yml中配置了
grails> run-app -Dgrails.server.servlet.context-path=/helloworld
| Running application...
...
...
Grails application running at http://localhost:8080/helloworld in environment: development
<=======<==========---> 83% EXECUTING [24s]
> :bootR> :bootRun
grails>
也可以在命令行中指定端口号,端口号范围在1024 ~ 49151 。
grails> run-app -Dgrails.server.servlet.context-path=/helloworld -port=9999
| Running application...
...
...
Grails application running at http://localhost:9999/helloworld in environment: development
<=======<==========---> 83% EXECUTING [43s]
> :bootR> :bootRun
grails>
可以在grails交互模式中,输入quit命令来退出交互模式并停止项目
也可以不在grails交互模式中启动项目,直接在外面使用命令启动:➜ /Users/xiaosan/Documents/work/helloworldgrails4 >grails run-app -Dgrails.server.servlet.context-path=/helloworld -port=9999
官方说最好以交互模式启动应用程序,因为容器重启要快得多,但实际使用时没感觉快多少。
⎮如何选择开发工具
推荐使用IDEA
其他可选开发工具:
- 可以通过 Sublime 文本编辑器的 Sublime Package Control 安装插件sublimetext-grails
- 将 VIM 设置为 Grails 编辑器,参考using-vim-as-your-grails-ide-part-1-navigating-your-project
- Atom 编辑器安装插件atom-grails
- Visual Studio Code安装扩展code-groovy。
IDEA如何导入Grails项目
选择 File / Open,
选择grails项目的 build.gradle文件。
以项目形式打开。
信任项目
等待导入配置
导入完成
⎮目录结构,约定优于配置
Grails 使用“约定优于配置”来配置自身。意味着Grails的文件名称和位置不需要显示的去配置,按照Grails的默认约定就可以了,因此需要熟悉 Grails 提供的目录结构。
- grails-app:Groovy 源码的顶级目录
- conf:存放配置文件的目录,可以配置日志,数据源,Spring bean配置等。
- controllers:Web控制器,MVC中的C。负责处理请求并创建或准备响应,控制器可以直接生成响应或委托给视图。
- doamin:领域类,MVC中的M。定义业务流程的状态和行为,配置属性约束,配置一对一、一对多或多对多关系。
- i18n:支持开箱即用的国际化(i18n)。Grails底层利用SpringMVC的国际化支持。
- services:业务层。用于存放应用程序逻辑,让控制器负责处理带有重定向等的请求。
- taglib:标签库。
- utils:Grails 特定的实用程序。
- views:Groovy 服务器页面或 JSON 视图 - MVC 中的 V。
- src/main/scripts:代码生成脚本,用于创建脚本,类似月create-app、run-app脚本。
- src/main/groovy:存放groovy代码
- src/test/groovy:存放单元测试
⎮如何部署Grails项目
1. 使用默认内嵌的Tomcat8部署
- 先打包成war文件
➜ /Users/xiaosan/Documents/work/helloworldgrails4 >grails war
Processing File 2 of 43 - bootstrap.bundle.js
...
Processing File 43 of 43 - bootstrap.min.css.map
> Task :assetCompile
Finished Precompiling Assets
BUILD SUCCESSFUL in 15s
7 actionable tasks: 5 executed, 2 up-to-date
| Built application to build/libs using environment: production
- 默认打包环境为生产环境。
打包文件默认生成在build/libs/helloworldgrails4-0.1.war 。
默认情况下,Grails 将在 WAR 文件中包含可嵌入版本的 Tomcat - 使用java命令启动内嵌Tomcat的war文件
➜ /Users/xiaosan/Documents/work/helloworldgrails4 >java -Dgrails.env=prod -jar build/libs/helloworldgrails4-0.1.war
Grails application running at http://localhost:8080 in environment: production
- 启动可以添加一些参数:
-Dgrails.env参数可以指定启动的环境,不加这个参数默认是生产环境。
例如:
-Dgrails.env=prod:生产环境
-Dgrails.env=dev:开发环境
-Dgrails.env=test:测试环境
使用 -server 参数给JVM容器分配内存。
例如:-server -Xmx768M -XX:MaxPermSize=256m
参数一起使用:
➜ /Users/xiaosan/Documents/work/helloworldgrails4 >java -Dgrails.env=prod -server -Xmx768M -XX:MaxPermSize=256m -jar build/libs/helloworldgrails4-0.1.war
Java HotSpot(TM) 64-Bit Server VM warning: ignoring option MaxPermSize=256m; support was removed in 8.0
Grails application running at http://localhost:8080 in environment: production
启动成功访问http://localhost:8080,注意这里我将application.yml中的上下文路径给删除了,没有配置上下文路径。
由于MaxPermSize参数在JDK8中已经删除了,所以这里可以去掉这个参数。
jvm参数含义
-vmargs -Xms128M -Xmx512M -XX:PermSize=64M -XX:MaxPermSize=128M
-vmargs 说明后面是VM的参数,所以后面的其实都是JVM的参数了
-Xms128m JVM初始分配的堆内存
-Xmx512m JVM最大允许分配的堆内存,按需分配
-XX:PermSize=64M JVM初始分配的非堆内存
-XX:MaxPermSize=128M JVM最大允许分配的非堆内存,按需分配
2. 使用外部Tomtcat8部署
- 打包前修改build.gradle 文件,在依赖项中将 Tomcat 依赖的范围更改为provided
compile "org.springframework.boot:spring-boot-starter-tomcat"
改为provided "org.springframework.boot:spring-boot-starter-tomcat"
如果使用IDEA工具修改,改后后记得gradle图标,重新加载依赖 - 开始打包,进入项目所在目录,执行
grails war
命令。
| Resolving Dependencies. Please wait...
CONFIGURE SUCCESSFUL in 1s
BUILD SUCCESSFUL in 6s
7 actionable tasks: 6 executed, 1 up-to-date
| Built application to build/libs using environment: production
- 下载Tomcat8,解压到目录/opt/apache-tomcat-8.5-grails4
- 删除/opt/apache-tomcat-8.5-grails4/webapps/ROOT目录
- 将war包复制到/opt/apache-tomcat-8.5-grails4/webapps/,并改名为ROOT.war
cp build/libs/helloworldgrails4-0.1.war /opt/apache-tomcat-8.5-grails4/webapps/ROOT.war
- 修改Tomcat的端口号。修改/opt/apache-tomcat-8.5-grails4/conf/server.xml,将8080端口改为8880
- 启动Tomcat。进入/opt/apache-tomcat-8.5-grails4/bin目录,执行
➜ /opt/apache-tomcat-8.5-grails4/bin >./startup.sh
Using CATALINA_BASE: /opt/apache-tomcat-8.5-grails4
Using CATALINA_HOME: /opt/apache-tomcat-8.5-grails4
Using CATALINA_TMPDIR: /opt/apache-tomcat-8.5-grails4/temp
Using JRE_HOME: /Library/Java/JavaVirtualMachines/jdk1.8.0_171.jdk/Contents/Home
Using CLASSPATH: /opt/apache-tomcat-8.5-grails4/bin/bootstrap.jar:/opt/apache-tomcat-8.5-grails4/bin/tomcat-juli.jar
Tomcat started.
- 查看tomcat日志输出
➜ /opt/apache-tomcat-8.5-grails4/bin >tail -f ../logs/catalina.out
...
...
04-Aug-2021 16:02:03.911 信息 [main] org.apache.coyote.AbstractProtocol.start 开始协议处理句柄["http-nio-8880"]
04-Aug-2021 16:02:03.918 信息 [main] org.apache.coyote.AbstractProtocol.start 开始协议处理句柄["ajp-nio-8809"]
04-Aug-2021 16:02:03.920 信息 [main] org.apache.catalina.startup.Catalina.start Server startup in 13480 ms
- 访问项目http://localhost:8080
注意编译的时候依赖的是Tomcat8,打包后的war只能在Tomcat8中运行,不能放在Tomcat7中运行。
3. 使用外部Tomcat7部署
- 修改build.gradle文件,修改Tomcat的依赖版本,例如改为7.0.88,在dependencies{}上面添加
ext['tomcat.version'] = '7.0.88'
- 打包前修改build.gradle 文件,在依赖项中将 Tomcat 依赖的范围更改为provided
compile "org.springframework.boot:spring-boot-starter-tomcat"
改为provided "org.springframework.boot:spring-boot-starter-tomcat"
- 如果IDEA修改的build.gradle,修改后注意点击gradle图标重新加载依赖
- 开始打包,进入项目所在目录,执行
grails war
命令。
| Resolving Dependencies. Please wait...
CONFIGURE SUCCESSFUL in 1s
Processing File 7 of 43 - application.js
...
...
> Task :assetCompile
Finished Precompiling Assets
BUILD SUCCESSFUL in 9s
7 actionable tasks: 5 executed, 2 up-to-date
| Built application to build/libs using environment: production
- 下载Tomcat7.0.88,解压到目录/opt/apache-tomcat-7.0.88
- 删除/opt/apache-tomcat-7.0.88/webapps/ROOT目录
- 将war包复制到/opt/apache-tomcat-7.0.88/webapps/,并改名为ROOT.war
cp build/libs/helloworldgrails4-0.1.war /opt/apache-tomcat-7.0.88/webapps/ROOT.war
- 修改Tomcat7的端口号。修改/opt/apache-tomcat-7.0.88/conf/server.xml,将8005端口改为8705,将8080改为8780,将8009改为8709。该端口主要是为了避免多个Tomcat端口冲突,如果只有一个Tomcat就可以不用修改
- 启动Tomcat。进入/opt/apache-tomcat-7.0.88/bin目录,执行./startup.sh
➜ /opt/apache-tomcat-7.0.88/bin >./startup.sh
Using CATALINA_BASE: /opt/apache-tomcat-7.0.88
Using CATALINA_HOME: /opt/apache-tomcat-7.0.88
Using CATALINA_TMPDIR: /opt/apache-tomcat-7.0.88/temp
Using JRE_HOME: /Library/Java/JavaVirtualMachines/jdk1.8.0_171.jdk/Contents/Home
Using CLASSPATH: /opt/apache-tomcat-7.0.88/bin/bootstrap.jar:/opt/apache-tomcat-7.0.88/bin/tomcat-juli.jar
Tomcat started.
- 查看tomcat日志输出
八月 04, 2021 7:59:54 下午 org.apache.catalina.loader.WebappClassLoaderBase validateJarFile
信息: validateJarFile(/opt/apache-tomcat-7.0.88/webapps/ROOT/WEB-INF/lib/el-api-2.1.2-b03.jar) - jar not loaded. See Servlet Spec 3.0, section 10.7.2. Offending class: javax/el/Expression.class
八月 04, 2021 7:59:54 下午 org.apache.catalina.loader.WebappClassLoaderBase validateJarFile
信息: validateJarFile(/opt/apache-tomcat-7.0.88/webapps/ROOT/WEB-INF/lib/javax.el-3.0.0.jar) - jar not loaded. See Servlet Spec 3.0, section 10.7.2. Offending class: javax/el/Expression.class
八月 04, 2021 7:59:54 下午 org.apache.catalina.loader.WebappClassLoaderBase validateJarFile
信息: validateJarFile(/opt/apache-tomcat-7.0.88/webapps/ROOT/WEB-INF/lib/javax.el-api-3.0.1-b06.jar) - jar not loaded. See Servlet Spec 3.0, section 10.7.2. Offending class: javax/el/Expression.class
八月 04, 2021 7:59:54 下午 org.apache.catalina.loader.WebappClassLoaderBase validateJarFile
信息: validateJarFile(/opt/apache-tomcat-7.0.88/webapps/ROOT/WEB-INF/lib/javax.servlet-api-4.0.1.jar) - jar not loaded. See Servlet Spec 3.0, section 10.7.2. Offending class: javax/servlet/Servlet.class
八月 04, 2021 7:59:56 下午 org.apache.catalina.startup.TldConfig execute
信息: At least one JAR was scanned for TLDs yet contained no TLDs. Enable debug logging for this logger for a complete list of JARs that were scanned but no TLDs were found in them. Skipping unneeded JARs during scanning can improve startup time and JSP compilation time.
2021-08-04 20:00:02.899 ERROR --- [ost-startStop-1] o.s.boot.SpringApplication : Application run failed
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'requestMappingHandlerAdapter' defined in org.springframework.web.servlet.config.annotation.DelegatingWebMvcConfiguration: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter]: Factory method 'requestMappingHandlerAdapter' threw exception; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'mvcValidator' defined in org.springframework.web.servlet.config.annotation.DelegatingWebMvcConfiguration: Invocation of init method failed; nested exception is java.lang.NoClassDefFoundError: javax/el/ELManager
报错nested exception is java.lang.NoClassDefFoundError: javax/el/ELManager
因为Grails4.0.11对el-api的版本要求比较高,Tomcat7.0.88/lib/目录里自带的el-api.jar是2.1版本的,版本比较底,启动tomcat时先加载自己的底el-api.jar,后面就不会再去加载ROOT/WEB-INF/lib/目录下的高版本el-api.jar了。
执行➜ /opt/apache-tomcat-7.0.88/bin >./shutdown.sh
停止tomcat
复制/opt/apache-tomcat-7.0.88/webapps/ROOT/WEB-INF/lib/javax.el-api-3.0.1-b06.jar
到/opt/apache-tomcat-7.0.88/lib
重新启动Tomcat7
八月 04, 2021 8:08:40 下午 org.apache.catalina.startup.HostConfig deployDirectory
信息: Deployment of web application directory /opt/apache-tomcat-7.0.88/webapps/examples has finished in 149 ms
八月 04, 2021 8:08:40 下午 org.apache.catalina.startup.HostConfig deployDirectory
信息: Deploying web application directory /opt/apache-tomcat-7.0.88/webapps/host-manager
八月 04, 2021 8:08:40 下午 org.apache.catalina.startup.TldConfig execute
信息: At least one JAR was scanned for TLDs yet contained no TLDs. Enable debug logging for this logger for a complete list of JARs that were scanned but no TLDs were found in them. Skipping unneeded JARs during scanning can improve startup time and JSP compilation time.
八月 04, 2021 8:08:40 下午 org.apache.catalina.startup.HostConfig deployDirectory
信息: Deployment of web application directory /opt/apache-tomcat-7.0.88/webapps/host-manager has finished in 35 ms
八月 04, 2021 8:08:40 下午 org.apache.coyote.AbstractProtocol start
信息: Starting ProtocolHandler ["http-bio-8780"]
八月 04, 2021 8:08:40 下午 org.apache.coyote.AbstractProtocol start
信息: Starting ProtocolHandler ["ajp-bio-8709"]
八月 04, 2021 8:08:40 下午 org.apache.catalina.startup.Catalina start
信息: Server startup in 12017 ms
启动成功,访问http://localhost:8780
注意高版本编译的代码,不要使用底版本的servlet容器运行,很容易出现由于jar包不兼容导致冲突,最好使用默认的servlet容器来运行。
⎮Grails4支持的JavaEE容器有哪些
Grails 可以在任何支持 Servlet 3.0 及更高版本的容器上运行,并且可以在以下特定的容器产品上运行:
- Tomcat 7
- GlassFish 3 或以上版本
- Resin 4 或以上版本
- JBoss 6 或以上版本
- Jetty 8 或以上版本
- Oracle Weblogic 12c 或以上版本
- IBM WebSphere 8.0 或以上版本
Grails4.0.11默认是使用Tomcat8作为内嵌servlet容器,部署时也最好使用Tomcat8。
⎮如何快速创建控制器和领域类
1. 创建领域类
Grails提供了一些create-*命令,用于快速创建应用。当然使用IDEA创建更快。
例如:创建领域类命令create-domain-classgrails create-domain-class book
这将会创建一个helloworldgrails4.Book.groovy领域类
和对应的测试类helloworldgrails4.BookSpec.groovy
创建时没有给book指定包名,默认使用项目名作为包名。
➜ /Users/xiaosan/Documents/work/helloworldgrails4 >grails create-domain-class book
| Created grails-app/domain/helloworldgrails4/Book.groovy
| Created src/test/groovy/helloworldgrails4/BookSpec.groovy
查看创建的领域类
package helloworldgrails4
class Book {
static constraints = {
}
}
修改领域类,添加两个字段
package helloworldgrails4
class Book {
String title //书名
Date releaseDate = new Date() //发布日期
static constraints = {
}
}
2. 使用脚手架创建控制器和视图
Grails提供了一些脚手架功能用来快速生成应用程序的骨架。
命令以 generate-* 开头,例如 generate-all,它将生成控制器(及其单元测试)和相关视图:
grails generate-all Book -force
-force 参数强制生成覆盖已有文件。
➜ /Users/xiaosan/Documents/work/helloworldgrails4 >grails generate-all Book -force
| Rendered template Controller.groovy to destination grails-app/controllers/helloworldgrails4/BookController.groovy
| Rendered template Service.groovy to destination grails-app/services/helloworldgrails4/BookService.groovy
| Rendered template Spec.groovy to destination src/test/groovy/helloworldgrails4/BookControllerSpec.groovy
| Rendered template ServiceSpec.groovy to destination src/integration-test/groovy/helloworldgrails4/BookServiceSpec.groovy
| Scaffolding completed for grails-app/domain/helloworldgrails4/Book.groovy
| Rendered template show.gsp to destination grails-app/views/book/show.gsp
| Rendered template index.gsp to destination grails-app/views/book/index.gsp
| Rendered template create.gsp to destination grails-app/views/book/create.gsp
| Rendered template edit.gsp to destination grails-app/views/book/edit.gsp
| Views generated for grails-app/domain/helloworldgrails4/Book.groovy
自动生成的有:
- 控制器:grails-app/controllers/helloworldgrails4/BookController.groovy
- 业务层:grails-app/services/helloworldgrails4/BookService.groovy
- 控制器测试:src/test/groovy/helloworldgrails4/BookControllerSpec.groovy
- 业务层测试:src/integration-test/groovy/helloworldgrails4/BookServiceSpec.groovy
- 详情页面:grails-app/views/book/show.gsp
- 列表页面:grails-app/views/book/index.gsp
- 添加页面:grails-app/views/book/create.gsp
- 修改页面:grails-app/views/book/edit.gsp
grails-app/controllers/helloworldgrails4/BookController.groovy代码
package helloworldgrails4
import grails.validation.ValidationException
import static org.springframework.http.HttpStatus.*
class BookController {
BookService bookService
static allowedMethods = [save: "POST", update: "PUT", delete: "DELETE"]
def index(Integer max) {
params.max = Math.min(max ?: 10, 100)
respond bookService.list(params), model:[bookCount: bookService.count()]
}
def show(Long id) {
respond bookService.get(id)
}
def create() {
respond new Book(params)
}
def save(Book book) {
if (book == null) {
notFound()
return
}
try {
bookService.save(book)
} catch (ValidationException e) {
respond book.errors, view:'create'
return
}
request.withFormat {
form multipartForm {
flash.message = message(code: 'default.created.message', args: [message(code: 'book.label', default: 'Book'), book.id])
redirect book
}
'*' { respond book, [status: CREATED] }
}
}
def edit(Long id) {
respond bookService.get(id)
}
def update(Book book) {
if (book == null) {
notFound()
return
}
try {
bookService.save(book)
} catch (ValidationException e) {
respond book.errors, view:'edit'
return
}
request.withFormat {
form multipartForm {
flash.message = message(code: 'default.updated.message', args: [message(code: 'book.label', default: 'Book'), book.id])
redirect book
}
'*'{ respond book, [status: OK] }
}
}
def delete(Long id) {
if (id == null) {
notFound()
return
}
bookService.delete(id)
request.withFormat {
form multipartForm {
flash.message = message(code: 'default.deleted.message', args: [message(code: 'book.label', default: 'Book'), id])
redirect action:"index", method:"GET"
}
'*'{ render status: NO_CONTENT }
}
}
protected void notFound() {
request.withFormat {
form multipartForm {
flash.message = message(code: 'default.not.found.message', args: [message(code: 'book.label', default: 'Book'), params.id])
redirect action: "index", method: "GET"
}
'*'{ render status: NOT_FOUND }
}
}
}
grails-app/services/helloworldgrails4/BookService.groovy代码:
package helloworldgrails4
import grails.gorm.services.Service
@Service(Book)
interface BookService {
Book get(Serializable id)
List<Book> list(Map args)
Long count()
void delete(Serializable id)
Book save(Book book)
}
grails-app/views/book/show.gsp代码:
<!DOCTYPE html>
<html>
<head>
<meta name="layout" content="main" />
<g:set var="entityName" value="${message(code: 'book.label', default: 'Book')}" />
<title><g:message code="default.show.label" args="[entityName]" /></title>
</head>
<body>
<a href="#show-book" class="skip" tabindex="-1"><g:message code="default.link.skip.label" default="Skip to content…"/></a>
<div class="nav" role="navigation">
<ul>
<li><a class="home" href="${createLink(uri: '/')}"><g:message code="default.home.label"/></a></li>
<li><g:link class="list" action="index"><g:message code="default.list.label" args="[entityName]" /></g:link></li>
<li><g:link class="create" action="create"><g:message code="default.new.label" args="[entityName]" /></g:link></li>
</ul>
</div>
<div id="show-book" class="content scaffold-show" role="main">
<h1><g:message code="default.show.label" args="[entityName]" /></h1>
<g:if test="${flash.message}">
<div class="message" role="status">${flash.message}</div>
</g:if>
<f:display bean="book" />
<g:form resource="${this.book}" method="DELETE">
<fieldset class="buttons">
<g:link class="edit" action="edit" resource="${this.book}"><g:message code="default.button.edit.label" default="Edit" /></g:link>
<input class="delete" type="submit" value="${message(code: 'default.button.delete.label', default: 'Delete')}" onclick="return confirm('${message(code: 'default.button.delete.confirm.message', default: 'Are you sure?')}');" />
</fieldset>
</g:form>
</div>
</body>
</html>
grails-app/views/book/index.gsp代码
<!DOCTYPE html>
<html>
<head>
<meta name="layout" content="main" />
<g:set var="entityName" value="${message(code: 'book.label', default: 'Book')}" />
<title><g:message code="default.list.label" args="[entityName]" /></title>
</head>
<body>
<a href="#list-book" class="skip" tabindex="-1"><g:message code="default.link.skip.label" default="Skip to content…"/></a>
<div class="nav" role="navigation">
<ul>
<li><a class="home" href="${createLink(uri: '/')}"><g:message code="default.home.label"/></a></li>
<li><g:link class="create" action="create"><g:message code="default.new.label" args="[entityName]" /></g:link></li>
</ul>
</div>
<div id="list-book" class="content scaffold-list" role="main">
<h1><g:message code="default.list.label" args="[entityName]" /></h1>
<g:if test="${flash.message}">
<div class="message" role="status">${flash.message}</div>
</g:if>
<f:table collection="${bookList}" />
<div class="pagination">
<g:paginate total="${bookCount ?: 0}" />
</div>
</div>
</body>
</html>
grails-app/views/book/create.gsp代码:
<!DOCTYPE html>
<html>
<head>
<meta name="layout" content="main" />
<g:set var="entityName" value="${message(code: 'book.label', default: 'Book')}" />
<title><g:message code="default.create.label" args="[entityName]" /></title>
</head>
<body>
<a href="#create-book" class="skip" tabindex="-1"><g:message code="default.link.skip.label" default="Skip to content…"/></a>
<div class="nav" role="navigation">
<ul>
<li><a class="home" href="${createLink(uri: '/')}"><g:message code="default.home.label"/></a></li>
<li><g:link class="list" action="index"><g:message code="default.list.label" args="[entityName]" /></g:link></li>
</ul>
</div>
<div id="create-book" class="content scaffold-create" role="main">
<h1><g:message code="default.create.label" args="[entityName]" /></h1>
<g:if test="${flash.message}">
<div class="message" role="status">${flash.message}</div>
</g:if>
<g:hasErrors bean="${this.book}">
<ul class="errors" role="alert">
<g:eachError bean="${this.book}" var="error">
<li <g:if test="${error in org.springframework.validation.FieldError}">data-field-id="${error.field}"</g:if>><g:message error="${error}"/></li>
</g:eachError>
</ul>
</g:hasErrors>
<g:form resource="${this.book}" method="POST">
<fieldset class="form">
<f:all bean="book"/>
</fieldset>
<fieldset class="buttons">
<g:submitButton name="create" class="save" value="${message(code: 'default.button.create.label', default: 'Create')}" />
</fieldset>
</g:form>
</div>
</body>
</html>
grails-app/views/book/edit.gsp代码:
<!DOCTYPE html>
<html>
<head>
<meta name="layout" content="main" />
<g:set var="entityName" value="${message(code: 'book.label', default: 'Book')}" />
<title><g:message code="default.edit.label" args="[entityName]" /></title>
</head>
<body>
<a href="#edit-book" class="skip" tabindex="-1"><g:message code="default.link.skip.label" default="Skip to content…"/></a>
<div class="nav" role="navigation">
<ul>
<li><a class="home" href="${createLink(uri: '/')}"><g:message code="default.home.label"/></a></li>
<li><g:link class="list" action="index"><g:message code="default.list.label" args="[entityName]" /></g:link></li>
<li><g:link class="create" action="create"><g:message code="default.new.label" args="[entityName]" /></g:link></li>
</ul>
</div>
<div id="edit-book" class="content scaffold-edit" role="main">
<h1><g:message code="default.edit.label" args="[entityName]" /></h1>
<g:if test="${flash.message}">
<div class="message" role="status">${flash.message}</div>
</g:if>
<g:hasErrors bean="${this.book}">
<ul class="errors" role="alert">
<g:eachError bean="${this.book}" var="error">
<li <g:if test="${error in org.springframework.validation.FieldError}">data-field-id="${error.field}"</g:if>><g:message error="${error}"/></li>
</g:eachError>
</ul>
</g:hasErrors>
<g:form resource="${this.book}" method="PUT">
<g:hiddenField name="version" value="${this.book?.version}" />
<fieldset class="form">
<f:all bean="book"/>
</fieldset>
<fieldset class="buttons">
<input class="save" type="submit" value="${message(code: 'default.button.update.label', default: 'Update')}" />
</fieldset>
</g:form>
</div>
</body>
</html>
3. 使用脚手架生成的代码为我们实现了哪些功能
启动项目,看看Grails脚手架生成的代码为我们提供了哪些功能。
在这之前,将grails项目改为使用内嵌Tomcat方式启动,修改build.gradle
注释掉ext['tomcat.version'] = '7.0.88'
将provided "org.springframework.boot:spring-boot-starter-tomcat"
改为compile "org.springframework.boot:spring-boot-starter-tomcat"
使用grails run-app
命令启动项目
默认使用8080端口,没有配置上下文路径,打来浏览器访问http://localhost:8080
3.1 - 书籍列表页面
点击helloworldgrails4.BookConroller跳转到书籍列表页面http://localhost:8080/book/index
3.2 - 添加书籍页面
点击NewBook跳转到添加书籍页面http://localhost:8080/book/create
默认提供了领域类的属性表单,并且提供了非空验证。
添加一个书籍
点击Create保存跳转到书籍详情页面
3.3 - 书籍详情页面
点击Create后跳转到书籍详情页面http://localhost:8080/book/show/1
/book/show/1
/控制器/动作/ID
默认展示刚刚添加的属性信息,有添加成功提示信息
提供导航到书籍页面,添加书籍,修改书籍,删除书籍的链接。
3.4 - 修改书籍页面
点击Edit跳转到修改书籍页面http://localhost:8080/book/edit/1
默认提供了领域类属性表单,同样有领域类属性校验。
提供导航到书籍页面,添加书籍的链接。
点击Update跳转到书籍详情页面。
点击Create Book再次添加一本书籍
点击Book List跳转到书籍列表页面
3.5 - 删除书籍页面
先在列表页面点击要删除的书籍,进入书籍详情页面
点击“好” ,确认删除,删除成功后跳转到书籍列表页面
在列表页面显示删除成功的提示信息。
3.6 - 小结
脚手架为我们提供的功能有:
- 包含全部属性表单的添加页面
- 包含属性的列表页面,点击属性标题可以排序
- 包含全部属性表单的修改页面
- 包含全部属性表单的详情页面
- 包含删除数据功能
- 列表页面-详情页面-添加页面-修改页面的导航链接
- 操作数据后的提示信息
- 属性表单的校验(非空等)
基本上为我们提供了所有的数据操作功能,我们只需要修改默认的css样式就可以了。如果不喜欢默认的排版,就需要自己写页面布局,将属性一个一个的写出来了。
⎮完成入门
到目前为止,我们完成了从创建grails项目,到创建领域类,创建控制器,创建视图,完成增删改查,最后发布的所有流程。这只是一个简单的入门教程,主要先了解grails项目开发的几个大概步骤。Grails默认使用的H2内存数据库,数据库配置每次重启项目都会清空数据。后面会讲连接其他数据库,并持久化数据。