https://grails.org/ https://docs.grails.org/latest/ https://gsp.grails.org/latest/guide/index.html
Grails环境
下载程序包:grails-6.1.2.zip,bin目录添加到path中。
grails -Version
grails list-profiles
grails profile-info
grails create-app cn.duchaoqun --profile web -f hibernate5
grails create-app cn.duchaoqun -f asset-pipeline-grails
grails dev run-app
features
hibernate-validator, spring-boot-devtools, mockito, gorm-neo4j, cache-ehcache, asciidoctor, grails-web-console, scaffolding, database-migration, github-workflow-java-ci, asset-pipeline-grails, mongo-sync, spring-boot-starter-undertow, sqlserver, h2, assertj, grails-gsp, shade, jrebel, spring-boot-starter-jetty, postgres, micronaut-inject-groovy, cache, gorm-mongodb, views-markup, spring-boot-starter-tomcat, embedded-mongodb, logbackGroovy, hamcrest, gorm-hibernate5, micronaut-http-client, testcontainers, grails-console, mysql, views-json, geb, properties
IDEA: opt+command + g
配置文件:buildSrc/build.gralde
grails 6 中的新增的
repositories {
mavenCentral()
maven { url "https://repo.grails.org/grails/core/" }
}
dependencies {
implementation 'org.grails:grails-gradle-plugin:6.1.1'
}
配置文件:gradle.properties
grailsVersion=5.1.2 grailsGradlePluginVersion=5.1.1 groovyVersion=3.0.7 gorm.version=7.1.2 org.gradle.daemon=true org.gradle.parallel=true org.gradle.jvmargs=-Dfile.encoding=UTF-8 -Xmx1024M
配置文件:build.gradle 文件
// https://github.com/grails/grails-spring-security-core
implementation group: ‘org.grails.plugins’, name: ‘spring-security-core’, version: ‘5.0.0-RC1’, ext: ‘pom’
引用本地jar包
跟build.gradle 同级目录,创建libs目录,将jar都放到该目录中,然后再build.gradle中引用
dependencies {
implementation("io.micronaut:micronaut-http-client")
implementation("io.micronaut.groovy:micronaut-runtime-groovy")
runtimeOnly("ch.qos.logback:logback-classic")
implementation("io.micronaut:micronaut-validation")
compile fileTree(dir: 'libs', include: '*.jar')
}
配置文件:application.yml
server:
port: 80 // 服务端口
grails:
cors:
enabled: true // 开启CORS
数据库的dbCreate参数
有四个值可以选择 create-drop: 每次重启都会清空历史数据,开发环境使用,避免手动更新数据 update :生产的时候用这个,保留现有的数据,并且只在代码更改时更新表结构,但是Hibernate的更新支持是非常保守的,它不会进行任何可能导致数据丢失的更改,也不会检测重命名的列或表,因此大部分时候,我们需要手动添加和修改表结构。
配置UrlMapping
处理controler的一些规则。
注意:如果配合spring core使用,某些路径记得权限控制。
http://docs.grails.org/latest/guide/theWebLayer.html#applyingConstraints
https://guides.grails.org/grails_url_mappings/guide/index.html
class UrlMappings {
static mappings = {
// 默认的
// 直接访问 /controller 根据约定会调用 index action。
// 直接方法 /controller/action1 根据约定会调用 action1。
// 直接访问 /controller/action1/123 一般会取当前 controller 的对象作为参数,id就是123。
// format 参数一般是在开发 API 的时候用,例如 .xml .json 的时候用。
"/$controller/$action?/$id?(.$format)?"{
constraints {
// apply constraints here
}
}
"/"(controller:'login', action:'auth') // 访问指定 controller 的指定 action
"500"(view:'/error')
"404"(view:'/notFound')
}
}
配置logback.groovy
在3.x版本使用这个
import grails.util.BuildSettings
import grails.util.Environment
// See http://logback.qos.ch/manual/groovy.html for details on configuration
appender('STDOUT', ConsoleAppender) {
encoder(PatternLayoutEncoder) {
pattern = "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"
}
}
root(INFO, ['STDOUT'])
def targetDir = BuildSettings.TARGET_DIR
if (Environment.isDevelopmentMode() && targetDir)
{
appender("FULL_STACKTRACE", FileAppender) {
file = "${targetDir}/stacktrace.log"
append = true
encoder(PatternLayoutEncoder) {
pattern = "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"
}
}
logger("StackTrace", INFO, ['FULL_STACKTRACE'], false)
}
配置logback.xml
Use in version 5.x and 6.x;
<configuration>
<!-- 负责写日志的组件,两个必要的属性-->
<!-- name: appender的名称-->
<!-- class: appender全限定名称-->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<withJansi>false</withJansi>
<!-- 格式化日志 -->
<encoder>
<pattern>%cyan(%d{yyyy-MM-dd HH:mm:ss.SSS}) %gray([%thread]) %highlight(%-5level) %magenta(%logger{36}) - %msg%n
</pattern>
</encoder>
</appender>
<!-- 将日志写入文件 -->
<appender name="httpAccessLogAppender" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 是否追加日志 -->
<append>true</append>
<!-- 写入文件位置 -->
<file>access.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- daily rollover -->
<fileNamePattern>access-%d{yyyy-MM-dd}.log
</fileNamePattern>
<maxHistory>360</maxHistory>
</rollingPolicy>
<!-- 格式化日志 -->
<encoder>
<charset>UTF-8</charset>
<pattern>%cyan(%d{yyyy-MM-dd HH:mm:ss.SSS}) %gray([%thread]) %highlight(%-5level) %magenta(%logger{36}) - %msg%n
</pattern>
</encoder>
<immediateFlush>true</immediateFlush>
</appender>
<!-- 过滤日志,部分警告不影响程序运行,但是不美观,就需要详细设置。 -->
<logger name="org.hibernate.orm.deprecation">
<level value="off"/>
</logger>
<root level="INFO">
<appender-ref ref="STDOUT"/>
<appender-ref ref="httpAccessLogAppender"/>
</root>
</configuration>
Grails获取配置文件内容
在application.yml中配置如下地址:
api:
callBackUrl: “http://lcmp.rexen.com.cn/”
然后在代码中获取配置信息
// 在Controller中获取配置:
def config = grailsApplication.config.getProperty(‘api.userName’)
// 在Service中获取配置:
GrailsApplication grailsApplication
callBackUrl = grailsApplication.config.getProperty(‘api.callBackUrl’) + callBackUrl
Grails读取grails-app/conf目录中文件
// Grails 读取config目录中的文件
KeyStore readKeyStore() {
KeyStore clientStore = KeyStore.getInstance("PKCS12")
InputStream inputStream = this.class.classLoader.getResourceAsStream("duchaoqun.cn.p12")
clientStore.load(inputStream, "loveh129".toCharArray())
return clientStore
}
Controller
基本约定
用户浏览器访问:Controller中的Action,直接访问Action不携带任何内容。
http://localhost:8080/Collateral/vehicle
def vehicle()
访问Action携带一个id,实际根据约定可以获取对应的对象
http://localhost:8080/collateral/vehicle/78506
def vehicle(Vehicle vehicle)
针对Get和Post请求,根据约定,参数会存储在params对象中。
// 可以这样获取参数值
def max = params.max
def offset = params.offset
def collateralName = params[“userFullName”]
// 必要的时候需要使用?号,groovy语法,确保出错时产生程序问题
def externalId = params?.externalId
传递变量给GSP
// 这里可以将整个参数对象params传递给GSP页面进行操作,建议传递单个的值。
respond list, model: [collateralTeamCount: count, user: user, params:params]
// 在GSP页面上使用如下方式来获取数值,例如搜索框的回显问题。
value=”${params?.fullName}“
params
params参数
直接跳转到另外一个Action,params 中会拼到目标的URL上,类似浏览器使用 GET 方式访问了目标 Action。
redirect(action: ‘create’, params: [workflowTypeTeam: workflowTypeTeam])
多个redirect的情况
如下图,像这种在代码里面,一个 Action里面有两个redirct,一个由if控制,在第一个后面记得加上 return,它不会自己跳出,当顺序执行到外面的redirect的时候就报错了。
“Cannot issue a redirect(..) here. A previous call to redirect(..) has already redirected the response.”
在Action中返回一些对象
可以直接添加到 params
里面,然后在GSP页面上直接 params.xxx
的方式调用,这里可以不用写在 model 里面,默认GSP页面上就可以使用。
def create(WorkflowStage workflowStage) {
params.componentList = Component.findAllActive(true)
params.stage = workflowStage
respond new WorkflowEvent(params)
}
<g:select name="component.id" id="component"
value="${this.workflowEvent?.component?.id}"
from="${params.componentList}" optionKey="id" optionValue="name"
noSelection="['': '请选择']">
</g:select>
forward
一般用于对象不存在,然后直接服务跳转,不同于 redirect 让浏览器再访问一次别的Action。
forward action: "show"
forward controller: "book", action: "list"
forward action: "show", id: 4, params: [author: "Stephen King"]
forward controller: "book", action: "show"
redirect
从一个Controller的Action中直接执行另外一个Action,相应到浏览器的 URL 也会显示为目标的地址。
官方文档:http://docs.grails.org/latest/ref/Controllers/redirect.html
参考文档:https://tosbourn.com/difference-between-redirect-render-rails/
常规用法
根据约定,这里只传递过去ID就可以,框架会自动解析访问路径,如下两种方式是一样的。
// 可以直接传递对象,默认解析出来的就是opportunity/show/id
redirect opportunity
redirect action: "show", controller: "opportunity", id: [opportunity.id]
返回来源地址
在GSP页面A中,创建B对象,这里直接访问B对象的Create方法,将URL传递过去。
def create(){
params['targetUri'] = request.getHeader("referer")
respond new RightCertification(params)
}
然后B对象的Create页面,将URL地址放到表单里面,再传递给后面Save Action:
<g:hiddenField name=“targetUri” value=”${params?.targetUri}”>
最后B对象的Save Action就可以直接跳转回A页面
redirect url: params[‘targetUri’]
render
http://docs.grails.org/latest/ref/Controllers/render.html
1. 访问先通过Controller,如果没有render等操作,默认渲染Action同名的GSP文件。 2. 如果render其他页面,在客户端浏览器上的URL地址还是当前Action地址,但是页面内容是其他的页面,直接处理GPS页面,不通过目标GSP对应的Action方法。 3. 最后浏览器上显示的地址就是目标当前action地址。 4. 与respond的主要区别就是不用必须返回对象。
在同一个Controller中可以直接指定action名字。
render :action_name
响应为同一个Controller的index action。
render(view: “/index”)”
响应为其他 Controller 的页面。
render “widgets/show”
直接响应Json字符串。
render jsonOutput.prettyPrint(jsonRes)
过滤数据,然后返回一个model包含provinceList、provinceTotal、filterParams、params对象。
render(view: ‘index’,
model: [provinceList : filterPaneService.filter(params, Province),
provinceTotal: filterPaneService.count(params, Province),
filterParams : FilterPaneUtils.extractFilterParams(params),
params : params])
用于 restful 直接返回JSON写法:
def returnMap = ['status': '200', 'text': 'xxxxxx']
render returnMap as JSON
respond
http://docs.grails.org/latest/ref/Controllers/respond.html
// 为当前 respond 语句所在 Action 所对应的页面返回对象数据。
respond changeRecord,
// 响应changeRecord 给 show 页面,但是 URL 还是当前的 Action,不执行show页面对应的 Action。
respond changeRecord, view: 'show'
respond 必须返回一个“Domain对象或集合”,然后加上其他的model等,如果需要返回自定义的List对象需要放到Modle里面 在使用逻辑判断的时候,个别分支需要使用 return 语句。 根据“内容协商”配置的内容进行响应自动适应类型。
Example | Argument Type | Calculated Model Variable |
---|---|---|
respond Book.list() | java.util.List | bookList |
respond Book.get(1) | example.Book | book |
respond( [1,2] ) | java.util.List | integerList |
respond( [1,2] as Set ) | java.util.Set | integerSet |
respond( [1,2] as Integer[] ) | Integer[] | integerArray |
// 默认的 show 页面,传递一个对象,和一组其他对象。
def layout = Layout.get(id)
def layoutPanel = LayoutPanel.findAllByLayout(layout, [sort: ‘displayOrder’, order: ‘asc’])
respond layout, model: [layoutPanel: layoutPanel]
// 选择最合适的类型并转换格式进行响应
respond Book.get(1), formats: [‘xml’, ‘json’]
参数列表:
object必选参数:需要响应的对象,这个是必须有的!
可选的参数
view - The view to use in case of HTML rendering(相应的页面)
model - The model to use in case of HTML rendering(可以相应各种类型的数据)
status - The response status(相应状态)
formats - A list of formats to respond with
includes - Properties to include if rendering with the converters API
excludes - Properties to exclude if rendering with the converters API
通过设置 response 对象,我们来控制一些返回参数,
def downloadReport(ReportTemplate reportTemplate, String format) {
String jsonStr = ""
if (reportTemplate.component) {
jsonStr = componentService.evaluate(reportTemplate.component, dataSource)
}
OutputStream out = null
try {
HttpURLConnection con = (HttpURLConnection) (new URL(reportTemplate.fileUrl + "?format=" + format)).openConnection()
con.setConnectTimeout(30 * 1000)
con.setDoOutput(true)
con.setDoInput(true)
con.setRequestMethod("GET")
con.setRequestProperty("Content-Type", "application/json")
if (jsonStr) {
log.info("报告参数:" + jsonStr)
con.outputStream.withWriter { Writer writer -> writer << jsonStr }
}
def fileByte = con.inputStream.getBytes()
if (format == "PDF") {
response.setContentType("application/pdf")
out = response.getOutputStream()
out.write(fileByte, 0, fileByte.length)
out.flush()
out.close()
println("模板文件下载完成!")
} else if (format == "Excel") {
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
response.addHeader("Content-Disposition", "attachment;filename=report" + ".xlsx");
out = response.getOutputStream()
out.write(fileByte, 0, fileByte.length)
out.flush()
out.close()
println("模板文件下载完成!")
}
} catch (IOException e) {
e.printStackTrace()
render("报表服务异常,请检查接口地址:" + reportTemplate.fileUrl + "?format=" + format)
}
}
static allowedMethods
限制请求方法
class ApiController {
static allowedMethods = [
index: "POST", // 设置index方法只能使用POST请求来访问
test1: "POST,GET"] // 设置test1方法能使用POST和GET请求来访问
def index() {
render "这里是API访问接口。"
}
def test1() {
println(params)
render "Test for POST!"
}
static defaultAction
静态的属性 defaultAction 在直接通过URL访问该controller的时候,如果没有指定action名字,这里默认会访问的action是 def index() ,可以通过该属性指定其他Action。
class DefaultController {
static defaultAction = “admin”
…
}
withFormat
用来处理 request 类型,可以对应不同的需求。 在application.yml 中配置 mime 类型。
一般在保存对象的时候,通过 表单 提交这个对象进行保存,然后显示该对象信息。
如果没有提交表单,就响应其他内容。 基础用法:url 请求后面添加特定的后缀,http://localhost/default/test4.xml
def test4() {
request.withFormat {
json {
println "return json"
render Word.list() as JSON
}
xml {
println "return xml"
render Word.list() as XML
}
}
}
POST请求的 raw body
编写接口的时候,需要直接解析请求过来的JSON数据。 首先配置UrlMappings
class UrlMappings {
static mappings = {
// 在这里配置对应的方法,禁用默认的 parseRequest
"/ApiData/applyInvoice"(controller: 'ApiData', action: 'applyInvoice', parseRequest: false)
}
}
然后在Controller中我们自行解析对应的数据。 String requestsJsonRawBody = request.getReader().text
POST MutilFormData
因为需要给改方法设置POST的权限,然后直接从params里获取内容,file是Spring中的multipart对象,有对应的方法可以用,但是目前Grails3.x在IDEA中不能提示出来。
def uploadLocal() {
// 图片参数校验
def opportunityId = params[“opportunity”]
def attachmentType = params[“opportunity”]
def file = params.file
def fileOrgName = file?.getOriginalFilename()
// 将图片临时存储到 images目录中
def webrootDir = servletContext.getRealPath(”/”)
File fileImage = new File(webrootDir, ”${fileOrgName}”)
file.transferTo(fileImage)
about JSON
在Controller中处理三方请求的返回结果的时候,需要处理JSON格式的返回数据,可以按照如下方式进行控制。
import org.grails.web.json.JSONElement
import grails.converters.JSON
OkHttpClient client = utilService.httpsClient
.newBuilder()
.readTimeout(30, TimeUnit.SECONDS)
.build()
Request request = new Request.Builder()
.url("https://api.weixin.qq.com/sns/jscode2session")
.build()
Response response = client.newCall(request).execute()
String result = response.body().string()
JSONElement json = JSON.parse(result)
String openid = json["openid"]
获取模版等文件
在项目中我们可能会使用到一些模板文件和字体文件,需要Controller中获取
//文件路径
//src/main/resources/Alibaba-PuHuiTi-Regular.ttf
URL fontUrl = this.class.classLoader.getResource('NotoSerifCJKsc-Regular.otf')
String filePath = fontUrl.path
下载文件
指定ContentType来控制浏览器端下载到的文件类型,指定Content-Disposition来确定下载文件的名字:
def download(Attachments attachments) {
File file = new File(attachments?.filePath + attachments?.fileName)
String fileName = attachments?.fileName
String fileExtension = fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase()
String contentType = fileServerService.getContentType(fileExtension)
if (file.exists()) {
response.setHeader("Content-Disposition", "attachment; filename=\"" + URLEncoder.encode(attachments?.description, "UTF-8") + "\"")
response.setHeader("Content-Length", file.length() + "")
response.setContentType(contentType)
response.outputStream << file.newInputStream()
response.outputStream.flush()
} else {
render(["code": 200, "message": "文件不存在"] as JSON)
}
}
下载准备好的模板文件
- 将文件放置在 grails/src/resources/xxx.xlsx
- 在Controller中添加如下内容
def downloadTemplate() {
URL resource = this.class.classLoader.getResource('balanceSheetTemplate.xlsx') response.contentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
response.setHeader("Content-Disposition", "attachment; filename=balanceSheetTemplate.xlsx")
response.outputStream << resource.openStream().getBytes()
response.outputStream.flush()
response.outputStream.close()
}
Schedule
https://guides.grails.org/grails-scheduled/guide/index.html
https://guides.grails.org/grails3/grails-scheduled/guide/index.html
在3.x的版本中,貌似和https://plugins.grails.org/plugin/quartz 插件冲突,
可以使用:create-job AutoWorkflow 在 jobs目录中创建对应的任务类。
package com.next
import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j
import org.springframework.scheduling.annotation.Scheduled
import java.text.SimpleDateFormat
@Slf4j // 使用日志
@CompileStatic
class ScheduledJobService {
static lazyInit = false // 默认Service是 lazy 的,这里需要设置false才能触发
@Scheduled(fixedDelay = 10000L) // 每10s运行
void executeEveryTen() {
log.info "every 10 seconds :{}", new SimpleDateFormat("dd/M/yyyy hh:mm:ss").format(new Date())
}
}
execute task on target time
package com.next.zhongcheng
import com.next.SFTPService
import grails.gorm.transactions.Transactional
import groovy.transform.CompileStatic
import org.springframework.scheduling.annotation.Scheduled
@CompileStatic
@Transactional
class ParseFileService {
static boolean lazyInit = false
SFTPService SFTPService = new SFTPService()
@Scheduled(cron = "0 0 4 * * ?")
void test() {}
}
Note: only have 6 column, have no year column
0/2 * * * * ? 表示每2秒 执行任务 0 0/2 * * * ? 表示每2分钟 执行任务 0 0 2 1 * ? 表示在每月的1日的凌晨2点调整任务 0 15 10 ? * MON-FRI 表示周一到周五每天上午10:15执行作业 0 0 10,14,16 * * ? 每天上午10点,下午2点,4点 0 0/30 9-17 * * ? 朝九晚五工作时间内每半小时 0 0 12 ? * WED 表示每个星期三中午12点 0 0 12 * * ? 每天中午12点触发 0 15 10 ? * * 每天上午10:15触发 0 15 10 * * ? 每天上午10:15触发 0 15 10 * * ? 每天上午10:15触发 0 * 14 * * ? 在每天下午2点到下午2:59期间的每1分钟触发 0 0/5 14 * * ? 在每天下午2点到下午2:55期间的每5分钟触发 0 0/5 14,18 * * ? 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发 0 0-5 14 * * ? 在每天下午2点到下午2:05期间的每1分钟触发 0 10,44 14 ? 3 WED 每年三月的星期三的下午2:10和2:44触发 0 15 10 ? * MON-WED,SAT 周一至周三和周六的上午10:15触发 0 15 10 15 * ? 每月15日上午10:15触发 0 15 10 L * ? 每月最后一日的上午10:15触发 0 15 10 ? * 6L 每月的最后一个星期五上午10:15触发 0 15 10 ? * 6#3 每月的第三个星期五上午10:15触发
message
https://gsp.grails.org/latest/ref/Tags/message.html
获取 i18n 配置
在 message.properties 中定义 message 信息,然后再页面上直接使用。
// 取出 i18n 中配置的 accountCity.label 内容放置到 entityName 变量中,如果没取到,就设置为 AccountCity。
// 然后就可以在当前的 GSP 页面上使用这个变量。
<g:set var=“entityName” value=”${message(code: ‘accountCity.label’, default: ‘AccountCity’)}” />
// 直接将 message 插入到页面中
<g:message code=“default.home.label”/>
<g:message code=“default.button.create.label”/>
// 在JS里面使用统一的国际化 message
Service
在Service中使用查询
在 service 里面我们可以定义初始化方法来初始化数据,然后再 BootStrap 里面进行初始化。但是这里好像只能使用 Domain.find + SQL的方法,待深入学习。
static void init(){
if (ActivitySubtype.count() < 1){
println "init ActivitySubTy trrue)
new ActivitySubtype(name: "Sign In", type: ActivityType.find("from ActivityType where name = 'Call' ")).save(flush: true)
}
}
No hstore extension installed
在使用原生SQL查询的时候,传递的参数类型不一样会导致这个问题。
所有GSP内置标签以前缀g:开始。不像JSP,你不需要指定任何标签库的导入。 假如,一个标签以g:开始,它被自动认为是一个GSP标签。
可以在页面上直接调用 Service 对象,根据Service查询相关连的数据。
<g:set var="workflowInstanceService" value="${new com.next.WorkflowInstanceService()}"/>
<g:set var="flexFieldValue" value="${flexFieldInstanceService.getFlexFieldByName(it,"投标保函审批单","审批单编号")}"/>
// 在页面上添加引用
<%@ page import="cn.duchaoqun.dict.OrderType; cn.duchaoqun.Company;" %>
GSP
与JavaScript冲突的问题
${}
在GSP和JavaScript中都可以用来插入变量, 使用Grails框架的时候需要注意使用这个内容, 当必须使用的时候, 注意在Java 使用拼接的方式来实现, 不要使用该符号来插入变量.
const tbody = $('#taskLogList').empty();
response.list.forEach(logEntry => {
console.log(logEntry)
tbody.append(
`<tr>` +
`<td style="width: 50%">` + logEntry?.logs + `</td>` +
`<td style="width: 10%">` + logEntry?.status + `</td>` +
`<td style="width: 20%">` + logEntry?.startTime + `</td>` +
`<td style="width: 20%">` + logEntry?.endTime + `</td>` +
`</tr>`
);
});
g::link
超链接标签,官方参考https://gsp.grails.org/latest/ref/Tags/link.html
id属性
就是URL最后面的数字,绑定到Controller中的对象
http://localhost:8080/component/ajaxEvaluateComponent/1
<g:link action="applyInvoice" id="${invoice?.id}">开票</g:link>
// 然后在方法里面可以直接获取这个ID
def applyInvoice(Invoice invoice){}
params属性
就是Get参数
<g:link controller="someController" action="someAction" id="${it.id}" params="[format: 'Excel']">
按钮
</g:link>
<g:link name="openId" action="identity" params="[openId: openId, code:code, state:state]">
按钮
</g:link>
// 在Controller中直接能取到这个参数
def someAction(ReportTemplate reportTemplate,String format){}
// 注意,params里面的参数不用 ${} 的形式
resource 属性
可以访问其他 Domain 对应的 Controller 的 Action。
<g:link resource=”${DataCollectionItem}” action=“create” params=“[dataCollection: dataCollection.id]”>
添加
</g:link>
// 访问到一些网址的时候需要返回到来源地址,就需要做一些处理
def create() {
params[‘targetURL’] = request.getHeader(“referer”)
respond new DataCollectionItem(params)
}
target 属性
在新标签页打开网页与传统的HTML一样,使用target属性:target=“_blank”
g::textField
Creates a input of type ‘text’ (a text field). An implicit “id” attribute is given the same value as the name unless you explicitly specify one.
<g:textField name="myField" value="${myValue}" readonly="readonly" />
// 当你要显示一个从后来传过来的日期,并且要格式化的时候,可以结合formatDate
<g:textField name="date_3" placeholder="营业截止日期" id="date_3"
value="${formatDate(format: 'yyyy-MM-dd', date: account?.licenseEndTime)}"/>
- name (required) - The name of the text field
- value (optional) - The initial text to display in the text field. By default the text field will be empty.
- readonly(optional) - Readonly, can not edit it.
- required=“required” - HTML5前端验证必填内容
g::paginate
// Controller code like this.
def index() {
[faPiaos: FaPiao.list(params), faPiaoCount: FaPiao.count()]
}
// GSP 页面上使用如下配置
<g:paginate controller=“faPiao” action=“index” total=”${faPiaoCount}”/>
// 个别时候后端查询需要用到的参数可以直接写在 params属性中
<g:paginate controller=“leads” action=“indexByUser” params=”${[‘type’: ‘Account’]}”
total=”${leadsCount ?: 0}”/>
// 默认情况paginate会传给后端max和offset两个参数,用来处理分页语句。
List list = Leads.executeQuery(sql, sql_params, [max: params.max, offset: params.offset])
total (required) - The total number of results to paginate
action (optional) - the name of the action to use in the link; if not specified the default action will be linked
controller (optional) - the name of the controller to use in the link; if not specified the current controller will be linked
id (optional) - The id to use in the link
params (optional) - A Map of request parameters
prev (optional) - The text to display for the previous link (defaults to “Previous” as defined by default.paginate.prev property in the i18n messages.properties file)
next (optional) - The text to display for the next link (defaults to “Next” as defined by default.paginate.next property in the i18n messages.properties file)
omitPrev (optional) - If set to true, the previous link will never be shown
omitNext (optional) - If set to true, the next link will never be shown
omitFirst (optional) - If set to true, the first page link will only be shown when the first page is within the offset
omitLast (optional) - If set to true, the last page link will only be shown when the last page is within the offset
max (optional) - The number of records to display per page (defaults to 10). Used ONLY if params.max is empty
maxsteps (optional) - The number of steps displayed for pagination (defaults to 10). Used ONLY if params.maxsteps is empty
mapping (optional) - The named URL mapping to use to rewrite the link
offset (optional) - Used ONLY if params.offset is empty
g::form
https://gsp.grails.org/latest/ref/Tags/form.html
Official Demo
<g:form name="myForm" action="myaction" id="1">...</g:form>
results in:
<form action="/shop/book/myaction/1" method="post"
name="myForm" id="myForm">
...
</form>
<g:form name="myForm" url="[action:'list',controller:'book']">...</g:form>
results in:
<form action="/shop/book/list" method="post" name="myForm" id="myForm">
...
</form>
<g:form action="show">...</g:form>
results in:
<form action="/shop/book/show" method="post" >...</form>
框架用法
// 前端使用hiddenField来控制唯一id。
<g:form controller="company" action="save">
<g:hiddenField name="id" value="${company.id}"/>
<div class="row">
<div class="row">
<div class="col-sm-3">
<div class="form-group">
<label>行业</label>
<input type="text"
class="form-control"
title="行业"
name="industry"
value="${company.industry}">
</div>
</div>
</div>
<div class="card-footer">
<button type="submit" class="btn btn-primary">保存</button>
</div>
</g:form>
// 后端会自动更新这个对象。
@Transactional
def save(Company company) {
company.save flush:true
redirect action:"index"
}
// GSP 页面
<g:form controller="faPiao" action="update" id="${faPiao.id}">
<g:hiddenField name="targetUri" value="${params?.targetUri}"/>
<g:submitButton class="btn btn-primary btn-w-m" name="update" value="更新"/>
// Controller 中的 Action, here will get the target Object by id.
@Transactional
def update(Account account) {
account.save flush: true
redirect account
}
// 注意id属性,框架功能/controller/edit/id,用来直接控制对象。使用js提交表单的时候需要使用class来提交,不要使用id等关键字。
<g:form resource="${this.product}" method="PUT" class="form-horizontal productForm">
$("#productBtn").click(function () {
var markupStr = $('#summernote').summernote('code');
$("#introduction").val(markupStr);
$(".productForm").submit();
});
表单重复提交问题
// 在 form 标签上添加 useToken 属性。
<g:form useToken="true" action="show" class="form-horizontal">
// 在 Controller 里面如下处理。
withForm {
render "Somecode"
}.invalidToken{
render "请勿重新提交"
}
参数
action (optional) - The name of the action to use in the link; if not specified the default action will be linked controller (optional) - The name of the controller to use in the link; if not specified the current controller will be linked id (optional) - The id to use in the link fragment (optional) - The link fragment (often called anchor tag) to use mapping (optional) - The named URL mapping to use to rewrite the link params (optional) - A Map of request parameters url (optional) - A map containing the action,controller,id etc. relativeUri (optional) - Used to specify a uri relative to the current path. absolute (optional) - If true will prefix the link target address with the value of the grails.serverURL property from application.groovy, or http://localhost:; if there is no setting in application.groovy and not running in production. base (optional) - Sets the prefix to be added to the link target address, typically an absolute server URL. This overrides the behaviour of the absolute property, if both are specified. name (optional) - A value to use for both the name and id attribute of the form tag useToken (optional) - Set whether to send a token in the request to handle duplicate form submissions. See Handling Duplicate Form Submissions method (optional) - The form method to use, either POST or GET; defaults to POST
g::submitButton
Creates a submit button using the indicated value as the caption. JavaScript event handlers can be added using the same parameter names as in HTML.
<g:submitButton name="update" value="Update" type="submit"/>
name (required) - The name of the button
value (required) - The title of the button and name of action when not explicitly defined.
scope属性
选择变量被放入的范围可以使用scope属性
<g:set var=“now” value=”${new Date()}” scope=“request” />
page - 当前页面范围 (默认)
request - 当前请求范围
flash - flash作用域,因此它可以在下一次请求中有效
session - 用户session范围
application - 全局范围.
变量可以使用再JS里面alert(”${fileManager}“)
g::hiddenField
Creates a input of type ‘hidden’ (a hidden field). All the usual HTML elements apply.
<g:hiddenField name="myField" value="myValue" />
<g:hiddenField name="collateral.opportunity.id" value="${this.collateral?.opportunity?.id}"/>
<g:hiddenField name="collateral.createBy.id" value="${this.collateral?.createBy?.id}"/>
name (required) - The name of the text field value (optional) - The value of the text field
g::select
<g:select class="form-control"
name="pactNo"
from="${BoHai.list()}"
value="${opportunity.boHai.id}"
optionKey="id"
optionValue="brevityCode"
noSelection="${['': '请选择']}"/>
- name (required) - The name of the select element.
- from (required) - The list or range to select from这里取一个对象列表,optionKey和optionValue对应每个对象的属性值。
- value:默认显示在前端的值,如果这个id和from列表中的id对应,会呈现出对应的默认的值。
- optionKey:传递给后端的对应的pactNo的值,一般就是对象列表的id。
- optionValue:下拉列表中的body文字,一般是列表对象的某个属性值,也可以是多个属性拼接的值。
- required=“required”:必选项目。
- disabled=“false” 控制是否可以修改
- noSelection (optional) - A single-entry Map detailing the key and value to use for the “no selection made” choice in the select box. If there is no current selection this will be shown as it is first in the list(如果不加这个参数,默认选择列表中的第一个), and if submitted with this selected, the key that you provide will be submitted. Typically this will be blank - but you can also use ‘null’ in the case that you’re passing the ID of an object
自定义字典
params.custTypes = [[option: "农户", value: "01"],
[option: "工薪", value: "02"],
[option: "个体工商户", value: "03"],
[option: "学生", value: "04"],
[option: "其他", value: "99"]]
<g:select name="custType" from="${params.custTypes}"
optionValue="option"
optionKey="value"
class="select2_demo_3 form-control"
noSelection="['': '请选择客户类型']"/>
直接再GSP页面定义list中选择一个
<g:select class="from-control select2"
style="width: 100%;"
from="${['是', '否']}"
name="online">
</g:select>
optionValue
拼接值
<g:select name="comments" class="form-control"
from="${params.comments}"
optionKey="id"
optionValue="${{ comment ->
comment.stage.instance.name + '-' + comment.stage.name + '-' + comment.user.fullName + '-' + comment.comment
}}"/>
g::checkbox
<g:checkBox name="check1Value"
value="${opportunityCheck?.check1Value}"
class="form-control"
style="height:16px;margin-top: 10px"
/>
g::each
在gsp页面上的循环语法
<g:each in="${books}">
<p>Title: ${it.title}</p>
<p>Author: ${it.author}</p>
</g:each>
指定一个变量名称,这样更容易理解
<g:each var="book" in="${books}">
<p>Title: ${book.title}</p>
<p>Author: ${book.author}</p>
</g:each>
使用status字段,使用状态变量,在循环过程中,status从1开始增加,如果使用该变量,必须使用var变量。
<g:each status="i" in="${itemList}" var="item">
<!-- Alternate CSS classes for the rows. -->
<tr class="${ (i % 2) == 0 ? 'a' : 'b'}">
<td>${item.id?.encodeAsHTML()}</td>
<td>${item.parentId?.encodeAsHTML()}</td>
<td>${item.type?.encodeAsHTML()}</td>
<td>${item.status?.encodeAsHTML()}</td>
</tr>
</g:each>
g::formatDate
根据 SimpleDateFormat格式化 java.util.Date 实例。
<g:formatDate format="yyyy-MM-dd" date="${date}"/>
<g:formatDate date="${date}" type="datetime" style="MEDIUM"/>
<g:formatDate date="${date}" type="datetime" style="LONG" timeStyle="SHORT"/>
<g:formatDate date="${date}" type="time" style="SHORT"/>
date (必选) - 需要格式化的实例。
format (可选) - 格式。
formatName (可选) - 根据 i18n 文件配置的 default.date.format 来渲染格式,如果 format 和 formatName 都没指定,就使用 yyyy-MM-dd HH:mm:ss z
type (可选) - 指定显示 date 或者 time,如果设置了该项,format 和 formatName 都会失效。指定’date’仅显示日期,指定 ‘time’ 仅显示时间,指定’both’/‘datetime’显示日期和时间。
timeZone (可选) - 设置 timeZone。
locale (可选) - Force the locale for formatting.
style (可选) - Use default date/time formatting of the country specified by the locale. Possible values: SHORT (default), MEDIUM, LONG, FULL . See DateFormat for explanation.
dateStyle (可选) - Set separate style for the date part.
timeStyle (可选) - Set separate style for the time part.
g::formatNumber
Formats a number using the patterns defined by the DecimalFormat class. Also supports attributes used in the JSTL formatNumber tag.
Example of left padding a number with zeros: 9位数左边用0补全。
<g:formatNumber number=”${myNumber}” type=“number” minIntegerDigits=“9” />
Example of formatting a number showing 2 fraction digits: 最多两位小数
<g:formatNumber number=”${myNumber}” type=“number” maxFractionDigits=“2” />
Example of formatting a number showing 2 fraction digits, rounding with RoundingMode.HALF_DOWN:
<g:formatNumber number=”${myNumber}“
type=“number”
maxFractionDigits=“2”
roundingMode=“HALF_DOWN” />
最多保留两位小数,向下四舍五入。
GSP上格式化Decimal工具,可以嵌套到HTML的属性里面。
<input class="form-control"
name="appliedTotalPrice"
title="实际价值"
id="totalPrice"
type="number"
step="any"
value="<g:formatNumber number="${this.estate?.appliedTotalPrice ?: 0}" minFractionDigits="2" maxFractionDigits="2"/>">元
显示金融数字
<g:formatNumber number="${number71?.beginBalance}" format="#,##0.00"/>
g::if
<g:if test="${session.role == 'admin'}">
<%-- show administrative functions --%>
<g:else>
<%-- show basic functions --%>
g::if-else
在页面上直接判断内容
<g:if test="${params.SXCount == '0'}">
0
<g:else>
1
g::render
使用框架的 template 功能,避免很多重复的代码操作。在定义好模板之后,在页面上直接 render 即可。先定义 template ,创建模板文件layouts/_navCommon.gsp,然后编写以下内容。可以在 layouts 目录根据需求创建很多模板,需要留意文件名称前面的下划线。
<div class="row wrapper">
<div class="col-lg-12">
<div class="ibox">
<div class="ibox-title">
<h5>Template</h5>
<div class="ibox-tools">
<a class="collapse-link">
<i class="fa fa-chevron-up"></i>
</a>
</div>
</div>
</div>
</div>
</div>
然后再需要的页面上直接 render 即可。
<g:render template=“/layouts/navCommon”/>
https://gsp.grails.org/latest/ref/Tags/render.html
g::layout
grails-app/views/layouts
// 模板的这里加载子页的Title内容 // 模板的这里加载子页的Head内容
// 模板的这里加载子页的Body内容
g::sortableColumn
https://gsp.grails.org/latest/ref/Tags/sortableColumn.html
lproperty - name of the property relating to the field
ldefaultOrder (optional) - default order for the property; choose between ‘asc’ (default if not provided) and ‘desc’
ltitle (optional) - title caption for the column
ltitleKey (optional) - title key to use for the column, resolved against the message source
lparams (optional) - a Map containing request parameters
laction (optional) - the name of the action to use in the link; if not specified the list action will be used
lmapping (optional) - The named URL mapping to use to rewrite the link
g::set
// 直接使用 i18n 中的文件
<g:set var=“entityName” value=“{new Date()}” />
// 标签内的文本内容就是变量的内容
<g:set var=“myHTML”>
Some re-usable code on: ${new Date()}
Websocket
https://github.com/zyro23/grails-spring-websocket
Domain
https://docs.grails.org/latest/ref/Domain%20Classes/Usage.html
static belongsTo = [city: City]
// 外键:会创建一个parent_id的外键,对应自己的父类,需要允许空
static belongsTo = [parent:District]
// 一对一外键
District district
static hasMany = [community: Community, address: Address]
// 约束关系
static constraints = {
code maxSize: 16, nullable: true
name maxSize: 32, comment: '处置部门' 设置备注名字
daysOfOtherRights nullable: true, blank: true
}
def beforeUpdate() {
modifiedDate = new Date()
}
String toString() {
name
}
static belongsTo
多对一的关系,会创建一个 city_id 对应 City 表中的 id 字段。
static hasMany
一对多关系,例如 Class 中有很多 Student,这里需要 Student 中设置 belongsTo Class 表然后再 Hibernate 查询的时候就可以直接使用 each in class.students 巴拉巴拉…
static constraints
nullable: true
限定 String 类型字段,允许一个 String 类型的字段被设置成 null(默认的都是非空字段)。
当表单的输入框没有值的时候,Web 请求提交的是 blank 字符串,而不是 null。在将多个值绑定的 property 的时候需要注意这点。使用默认的配置时,系统会将 blank 转换成 null,来对应系统默认设置的 nullable: false 我们可以通过如下地方修改这个默认转换行为 https://docs.grails.org/latest/ref/Constraints/nullable.html
blank: true
校验 String 类型字段是否可以是 null ,其他类型无效。
验证一个 String 类型的字段是否为 blank,其他类型设置无用。 column_name blank: false
如果一个 String 类型字段不能是 空白,就设置成 false。
Error Code: className.propertyName.blank
如果是 null 不会被该约束验证,应该使用 nullable 约束。
https://docs.grails.org/latest/ref/Constraints/blank.html
unique: true
记录的唯一性约束。
type: "text"
当某个列需要是类型,且不限制长度的时候(不是varchar(20)),数据库是 text 类型是使用。
inList: [‘Pending’,’End’]
限制字符串在指定的列表中,获取列表的内容。
<g:select class="form-control"
name="duration"
from="${Task.constrainedProperties.duration.inList}"
noSelection="${['': '期限']}"/>
def beforeUpdate(){}
这里需要确认,自定义方法?在更新前修改这个值?,是框架功能还是自定义功能?
static mapping
static mapping = {
table 'orders' // custom the table name by myself
}
级联删除
AccountBankAccount表关联bankAccount表,当删除AccountBankAccount表记录的时候级联删除掉bankAccount表里面的记录。
// Domain
class AccountBankAccount {
Account account
BankAccount bankAccount
static mapping = {
bankAccount cascade: 'delete'
}
}
// Controller, Notice this, delete A will delete B at the same time.
accountBankAccount.delete(flush: true)
extends
例如A表有 1、b、c 三个字段,我们要创建 B 表,B表需要有 a、b、c、d四个字段。这样我们就可以创建 B extends A,框架本身不会创建B表,它会在A表添加 d、class两个字段,然后把数据存放到 A表中。但是我们再Hibernate的使用上没有任何影响。
class 字段表示这个记录时哪个类的。
class Layout extends DataDictionary {
Integer wechatLayoutId = 0
}
save()
一个 Domain 对象在保存的数据库的时候,需要先经过验证,然后再保存,例如当某个字段超出长度、非空限制等等都会导致数据保存不上,但是这里不会给出错误信息,需要我们自己打印出来。
// 注意这里不要用 hasErrors method(validate 会更新 hasErrors的结果?)
if (result.validate()) {
result.save flush: true
} else {
println(result.errors)
respond result.errors, view: 'edit'
return
}
errors
- 返回的错误信息.
ResService saveType(DictType dictType, Map params) {
dictType.code = params.code as String
dictType.name = params.name as String
dictType.remark = params.remark as String
dictType.isActive = Boolean.parseBoolean(params.is_active as String)
if (dictType.validate()) {
dictType.save(flush: true)
return ResService.success(["成功": dictType.id])
} else {
StringBuilder errorMsg = new StringBuilder()
dictType.errors.allErrors.each {
errorMsg.append(MessageFormat.format(it.defaultMessage, it.arguments))
}
return ResService.fail(errorMsg.toString())
}
}
findAllBy
http://docs.grails.org/latest/ref/Domain Classes/findAllBy.html
// 使用排序
WorkflowInstanceLog log = WorkflowInstanceLog.findByStageAndStatusAndUser(it, "Success", workflowInstanceUser.user,[sort: "createdDate", order: "desc"])
// 查询某个字段不空的记录
findAllByParentIsNotNull()
listOrderBy*
Lists all of the instances of the domain class ordered by the property in the method expression
// everything
def results = Book.listOrderByAuthor()
// 10 results
def results = Book.listOrderByTitle(max: 10)
// 10 results, offset from 100
def results = Book.listOrderByTitle(max: 10, offset: 100, order: “desc”)
max - The maximum number to list
offset - The offset from the first result to list from
order - The order to list by, either “desc” or “asc”
https://docs.grails.org/latest/ref/Domain Classes/listOrderBy.html
转换Map
def usersMaps = db.eachRow(“select id,name from users”){
new User(it as Map)
}
createCriteria()
http://docs.grails.org/3.1.1/ref/Domain Classes/createCriteria.html
def opportunityContact = OpportunityContact.createCriteria()
def opportunityList = opportunityContact.list {
projections {
distinct “opportunity”
not { ‘in’(“opportunity”, dataCollectionItems) }
}
} as ArrayList
withCriteria()
def opportunityCheckList = OpportunityCheck.withCriteria {
eq 'user', user
eq 'status', params.status
eq('type', com.next.OpportunityType.findByName('融资担保'))
ge('lendingDate', sdf.parse(params['date_1']))
le('lendingDate', sdf.parse(params['date_2']))
isNotNull('lendingDate')
maxResults params.max
}
使用SQL
http://docs.grails.org/latest/ref/Domain%20Classes/findAll.html
CollateralType collateralType = CollateralType.findByName("居住用房")
// 注意:需要判断这个 List 是 null 的情况。
List<Long> collateralTypeList = CollateralType.findAllByParent(collateralType).id
def collateralSql = "from Collateral as a where 1=1"
// 注意这里的in写法,前面是 type.id 和后面的括号
collateralSql += " and a.type.id in (:collateralTypeList)"
dataList = Collateral.findAll(collateralSql, [collateralTypeList:collateralTypeList])
执行原始SQL
执行SQL语句,在Controller中定义dataSource,然后在Action中执行查询语句, 使用参数的SQL语句
class TestController {
def dataSource // 在Controller中定义 dataSource
def index = {
String sql = "select * from user"
def db = new groovy.sql.Sql (dataSource)
// 返回一个 List 结果集
List list = db.rows(sql)
// 执行一个语句,返回ture 和false,例如 create insert delete drop
db.execute(sql)
}
}
def getMenuList1() {
// 分页参数处理
int page = params.int('page') ?: 1
int perPage = params.int('perPage') ?: 10
// 提交给前端记得按照约定的变量名称命名
String sql = """
SELECT
m.name, m.url, m.status, m.order_number,
ps.name as page_schema_name
FROM menu m
LEFT JOIN page_schema ps ON m.page_schema_id = ps.id
WHERE 1=1
"""
Map paramsMap = new HashMap()
// 动态添加条件, 这里必须显示的给出参数的类型, 否则判断转换会失败
String name = params.name
if (name) {
sql += " AND m.name LIKE :name "
paramsMap.name = "%" + name + "%"
}
sql += " ORDER BY m.id DESC"
def sql1 = new Sql(dataSource)
params.items = sql1.rows(sql, paramsMap, (page - 1) * perPage, perPage)
params.total = sql1.rows(sql, paramsMap).size()
def res = ResService.success(params)
render res as JSON
}
多表关联
String sql = "select l from Leads as l, LeadsTeam as lt where lt.user = :user and l.id = lt.leads.id and l.type=:type "
List list = Leads.executeQuery(sql, sql_params, [max: params.max, offset: params.offset])
orderBy
params.sort = "createdDate"
params.order = "desc"
WorkflowInstanceLog workflowInstanceLog = WorkflowInstanceLog.findByStage(workflowInstanceStage,params)
plugin
Spring Security
文档地址:https://grails.github.io/grails-spring-security-core/snapshot/index.html 安全插件:implementation ‘org.grails.plugins:spring-security-core:6.1.1’
LoginController
登录:插件里面自带的登录用的LoginController/login/auth, 对应/views/login/auth.gsp 登录页面
正常我们应改直接访问这个目录:http://localhost:8080/login/auth,对应/views/login/auth.gsp 登录页面
验证:LoginController/login/authenticate 这个验证的方法在哪里?
创建用户类 grails-app/domain/com/mycompany/myapp/User.groovy 包含用户基本信息,插入前加密密码。
创建加密类:src/main/groovy/cn/duchaoqun/UserPasswordEncoderListener ,并注册到grails-app/conf/spring/resources.groovy 中,以供 springSecurityService 来调用,以插件的方式存在,就可以修改我们自定义的内容。
创建:class CustomUserDetailsService implements GrailsUserDetailsService
配置application.groovy,让组件可以找到用户相关的类,也可以配置到application.yml 文件中。
grails.plugin.springsecurity.userLookup.userDomainClassName = 'cn.duchaoqun.security.User'
grails.plugin.springsecurity.authority.className = 'cn.duchaoqun.security.Role'
grails.plugin.springsecurity.userLookup.authorityJoinClassName = 'cn.duchaoqun.security.UserRole'
Spring 升级之后,需要修改密码的存放方式:
implementation ‘org.grails.plugins:spring-security-core:6.1.1’
SpringSecurityUtils.securityConfig: 获取Spring 的配置信息。
/login/auth /login/auth.gsp
POST /login/authenticate 插件内置
spring-security-crypto-5.7.11.jar 加解密
org.springframework.security.crypto.password.DelegatingPasswordEncoder2a3sZ7l4OBqkQtISpco62j1u/WUHfZKHlEQ8YZNG7iYExUYV0kzDzdy
// 在Controller中获取已经登录的用户对象
User user = getAuthenticatedUser()
quartz
https://github.com/grails/grails-quartz https://grails.github.io/grails-quartz/snapshot/
* * * * * ? // 每秒钟执行 | 0/5 * * * * ? 每5秒执行
0 * * * * ? // 每分钟执行, 每分0秒 | 0 0/5 * * * ? 每五分钟执行
0 0 * * * ? // 每小时执行, 每时0分0秒 | 0 0 0/5 * * ? 每五小时执行
0 0 0 * * ? // 每一天执行, 每天0时0分0秒
0 0 0 1 * ? // 每个月执行, 每月1日0时0分0秒
0 0 0 1 1 ? // 每一年执行, 每年1月1日0时0分0秒
- 留意当Trigger以指定时间点开始, 以指定时间点结束的时候, 结束任务的方式.
// add in build.gralde
implementation "org.grails.plugins:quartz:3.0.0"
// note, it may need this.
implementation 'org.codehaus.groovy:groovy-templates:3.0.7'
//
.\gradlew.bat clean --refresh-dependencies
// get into grails
.\grailsw.bat
// show the help
grails> help create-job
| Command: create-job
| Description:
Creates a new Quartz scheduled job
| Usage:
grails create-job [JOB NAME]
| Arguments:
* Job Name - The name of the job (REQUIRED)
// create job
.\grailsw.bat create-job cn.duchaoqun.MyJob --stacktrace
Demo
//grails-app/jobs/cn.duchaoqun.MyJob
class MyJob {
// 触发器,当条件满足触发器的时候触发 execute() 内容。
static triggers = {
// 每 1000 毫秒 执行 execute() 方法
simple repeatInterval: 1000
// 每 1000 毫秒 执行 initTask() 方法
simple name: 'initTask', repeatInterval: 1000, repeatCount: 0
}
// 执行内容
void execute() {
print "Job run!"
}
}