本文简单介绍了体系的开发规范。

如果你拥有丰富的spring boot或grails的开发经验,会发现在项目中几乎保留了spring boot和grails的开发规约,使用起来非常方便。

1. 注意hibernate的自动保存

因为默认开启了openSessionInViewInterceptor,domain类会在controller的action方法执行完后,强制将当前transaction事务进行flush操作,从而自动保存domain类,可能的情况如下。

@Transactional   (1)
class ThingController {

    public String dosomething(OneDomain oneDomain){
        oneDomain.title = 'abc'            (2)
        return '/thing/dosomething'        (3)
    }
}
1 在controller层开启事务
2 对domain实例进行赋值操作
3 退出方法,并没有调用save方法
上面的情况会导致隐身调用save方法,从而将赋值操作的abc保存进数据库。 需要显式调用discard方法,放弃保存。
@Transactional
class ThingController {

    public String dosomething(OneDomain oneDomain){
        oneDomain.title = 'abc'
        oneDomain.discard()       (1)
        return '/thing/dosomething'
    }
}
1 显式调用discard方法,放弃保存

2. 开发工具及gradle支持

目前系统开发支持intellij IDEA 的社区版、旗舰版。系统构建推荐使用gradle。

系统默认的构建文件是在根目录下的build.gradle. 我们一般只需要关注gradle文件中两个点:1.repositories部分 2.dependencies部分

repositories {
    maven { url "https://repo1.maven.org/maven2/" }
}
dependencies {
        implementation('org.yunchen.gb:gb-core:X.X.X')
        implementation('org.yunchen.gb:gb-plugin-poi:X.X.X')
        
}

3. yml配置文件介绍

系统采用yml来记录系统的配置参数.系统默认的配置文件是在src/main/resources目录下的application.yml. yml中的配置分为系统配置、themleaf3页面配置、springsecurity安全配置、数据库配置等几个方面.

4. 数据源及多profile环境

yml分为development、test、production三个profile部分.通过spring.profiles.active键值来标记系统当前使用的profile.

系统分为3个数据环境设立,来避免数据层的数据噪音影响。 同时系统提供startup的启动运行类,通过幂等的数据初始操作,来初始化系统数据,详细参考相关的startup类章节。

配置文件中需要设置hibernate的配置内容,以确定数据库结构的自动生成逻辑.GORM组件会根据以上配置来同步数据库的数据结构。

5. application工程类和starup启动类

系统的application工程类,在生成工程的src/main/groovy/${package}/Application.groovy,是标准的spring boot 应用类,约定增加@ComponentScan(basePackages=["org.yunchen.gb"])注解

@ComponentScan(basePackages=["org.yunchen.gb"])
@SpringBootApplication
class DemoApplication extends SpringBootServletInitializer {

}

系统的startup启动运行类,在生成工程的src/main/groovy/${package}/init/Startup.groovy中。系统启动是会运行其中的init方法,系统关闭时会调用destroy方法。

支持多个使用GbBootstrap注解的启动类、多个init方法间互不影响. 执行顺序按照类的Order注解顺序,按照从小到大的顺序执行.

6. 动态插入和动态修改

尽管GORM默认使用version字段提供乐观锁防止脏读脏写,但为应付高并发修改或提供性能,同样需要只更新部分字段的动态更新功能 。

GORM并不支持Hibernate的DynamicUpdate 注解,而是在mapping闭包中有同样功能的dynamicUpdate、dynamicInsert方法。

        static mapping = {
                dynamicUpdate true
        //dynamicInsert true
        }

在domain类中添加后,可以看到save方法的输出的sql日志只会影响到修改的字段。(注意BaseUser类不可使用dynamicUpdate)

如个人账号信息中的当前余额字段的变更,要么使用dynamicUpdate谨慎变更,要么使用消息队列单线程线性修改,要么依托数据库保障高并发下面的顺序修改。

7. 微服务模式支持

为了从单体模式向微服务模式转换,需要从如下几个方面进行注意

7.1. spring security rest 组件

增加对rest 组件的引用,使用jwt来完成系统的无状态化管理

7.2. domain类的拆分

例如在单体应用中有两个domain类,person和order,表示用户和订单类

@Entity
class Person{
    String id
    String name
    String idcardNo
    。。。。
}

@Entity
class Order{
    long id
    timestamp orderCreated
    Person person        (1)
    。。。。
    static mapping = {
        table "goods_order"
    }
}

如下拆分为用户和订单两个微服务

//用户工程
@Entity
class Person{
    String id
    String name
    String idcardNo
    。。。。
}

//订单工程
@Entity
class Order{
    long id
    timestamp orderCreated
    String person_id          (2)
    String personEmbedInfo    (3)
    。。。。
    static mapping = {
        table "goods_order"
    }
}
1 单体应用保留数据库外键和约束
2 微服务拆分后,保留字段存储用户的uuid
3 避免跨微服务的查询过多,存储用户信息的json数据

7.3. 安全组件设置

可以修改安全组件的yml配置,不再从数据库中获取安全配置,用户角色信息;改为从yml配置中获取以上信息。

#spring security
security.basic.enabled: false
gb:
    springsecurity:
      csrf: disable
      cors: disable
      frameOptions: disabled   #disabled,deny,sameOrigin
      csrf: disable
      cors: enable
      corsConfig:
        allowCredentials: true # true or false
        allowedOrigins:  '*'  # * or http://localhost:8080
        allowedHeaders:  '*'  #
        allowedMethods:  '*' # GET,POST or *
        corsPath: /**
      headers:
        - {Access-Control-Expose-Headers: WWW-Authenticate,Authorization,Set-Cookie,X-Frame-Options}
        - {Access-Control-Max-Age: 3600}
      ajaxHeader: X-Requested-With
      password:
        encodeHashAsBase64: false
        algorithm: bcrypt # bcrypt,pbkdf2,SHA-512,SHA-384,SHA-256,SHA-224,SHA-1,MD5,MD2
      securityConfigType :  Requestmap
      requestMapWithoutDB: false                                 (1)
      interceptUrlMaps:
          -  {url: /webjars/**,configAttribute: permitAll }
          -  {url: /static/**,configAttribute: permitAll }
          -  {url: /js/**,configAttribute: permitAll }
          -  {url: /images/**,configAttribute: permitAll }
          -  {url: /css/**,configAttribute: permitAll }
          -  {url: /favicon.ico,configAttribute: permitAll }
          -  {url: /error/**,configAttribute: permitAll }
          -  {url: /login/**,configAttribute: permitAll }
          -  {url: /logout/**,configAttribute: permitAll }
          -  {url: /register/**,configAttribute: permitAll }
          -  {url: /jcaptcha/**,configAttribute: permitAll }
          -  {url: /dbconsole/**,configAttribute: hasAnyRole('ROLE_ADMIN'),httpMethod: POST}
          -  {url: /**,configAttribute: isFullyAuthenticated() }
      authorityWithoutDB: false                                     (2)
      authorityMaps:
          -  {id: 1,username: manager,password: manager,enabled: true,authorities: [ ROLE_ADMIN , ROLE_USER ]}
          -  {id: 2,username: user,password: user,enabled: true,authorities: [ ROLE_USER ]}
。。。。。。。。
1 此处改为true后,系统改为读取yml中的interceptUrlMaps替代Requestmap表
2 此处改为true后,系统改为读取yml中的authorityMaps替代BaseUser和BaseRole表

8. 内置注解及辅助服务类

类型 名称 描述

注解

@Title

用于描述类或字段

注解

@NoNeedRestTransBean

标记不用MVC组装的参数类(不支持domain类)

注解

@GbVersionJsonIgnoreFix

用于修复GORM在转json时无法输出version字段的bug

注解

@GbRestController

标记restful的controller类(自动扫描public方法)

注解

@GbController

标记controller类(自动扫描public方法)

注解

@GbInterceptor

标记Interceptor类(系统会自动注册)

注解

@GbDomainSimpleJsonFormat

用户domain类固定字段的json输出(常用于外键字段)

注解

@GbBootstrap

标记启动类(系统会根据Order注解的从小到大顺序执行启动类的init方法)

注解

@DomainAutoProperties

会为domain类注册一个setProperties(Map map)方法

辅助类

GbSpringUtils

用于spring的相关操作

8.1. GbSpringUtils类

GbSpringUtils类静态方法

action name 描述

getApplicationContext()

获取 应用context

getResource(String resource)

获取资源

getBean(String name)

获取bean

isDomain(String domainName)

是否domain类

getDomain(String domainName)

获取domain类

getDomainConstraintsMap(Class domainClass)

获取domain的约束定义

getConfiginfo(String key)

获取application.yml的配置信息

getI18nMessage(String code,List arguments,String defaultMessage,Locale locale)

获取i18n资源的信息

getI18nMessage(String code,List arguments,String defaultMessage)

获取i18n资源的信息

getI18nMessage(String code,List arguments)

获取i18n资源的信息

getI18nMessage(String code)

获取i18n资源的信息

publishEvent(Object event)

发布事件

publishEvent(AppEvent event)

发布系统事件

addApplicationListener(ApplicationListener<?> listener)

添加事件监听(订阅事件)

9. MVC简化

9.1. 默认约定

默认gb对spirng mvc进行了简化,约定domain类对应同名的controller,同时页面渲染使用同名的目录。

controller中的public方法自动映射为访问路径/${controllerName}/${actionName},默认区页面渲染引擎中定位 ${controllerName目录下的${action}文件。

默认的映射关系

action name view name 描述

index

index.html

列表首页

json

返回表格json数据

create

create.html

创建页面

save

保存处理返回json数据

edit

edit.html

修改页面

update

修改处理返回json数据

show

show.html

展示页面

detele

单条删除处理返回json数据

deteles

多条删除处理返回json数据

download

下载excel字节流

9.2. model层

model层默认都放置在/src/main/groovy/${package name}/domain目录下

系统使用GORM进行数据的对象关系映射ORMAPPING,因此默认会为每一个domain类提供id、version两个内置属性。
id默认是long型的自增主键.可以通过mapping闭包设置为sequence或UUID
内置乐观锁version,version字段是GORM内部维护的乐观锁,当数据发生修改时,version会自动增加1,系统使用它来判断是否发生了数据脏读,避免脏写。

9.3. controller

controller层默认都放置在/src/main/groovy/${package name}/controller目录下。

系统提供GbController和GbRestController两个注解

9.4. service

service层默认都放置在/src/main/groovy/${package name}/service目录下。

9.5. 自动组装参数类

系统扩展spring MVC的参数组装功能,提供基于domain类的自动组装,遵循如下原则:

提交表单参数中若没有id参数,则系统自动创建全新的domain对象,并将其余参数自动赋值。
如果提交表单参数中包含id参数,则系统会调用domain类的get(id)方法,获取domain类的数据库实例,并将其余参数自动赋值。
赋值过程中自动忽略version、clob、blob、byte[]类型的字段赋值。如是Date或Time类型的字段,会调用domain类上字段的@DateTimeFormat注解,来实现自动日期赋值。
如果提交表单参数中包含外键的参数,使用 referenceDomain.id的模式,如“baseUser.id”,赋值时,系统会自动调用findById(id)方法获取外键对象实例,赋值为domain对象。
详细参见工程中用户、角色、登录记录等默认实现

如果是前后端分离项目,或是restful的json请求:

无论angular,react,VUE ,访问服务端时都需要在header中增加如下配置

key 描述 value

X-Requested-With

标注访问模式

XMLHttpRequest

Content-Type

类型

application/json

Authorization

访问需要验证的地址时填写的认证信息:

Bearer ${access_token}

如果Content-Type=application/json的请求,框架会将发送的json组装成Map放到request的属性中,或是List类型放在request的属性中;如果转换失败,会将Object和错误信息放到request的属性中。
//Map
Map requestJsonMap=(Map) request.getAttribute(GbSpringUtils.GB_REQUEST_JSON_MAP)
//List
List requestJsonList=(List) request.getAttribute(GbSpringUtils.GB_REQUEST_JSON_LIST)
//Object
Object requestJsonObject=(Object) request.getAttribute(GbSpringUtils.GB_REQUEST_JSON_OBJECT)
String errInfo=(String) request.getAttribute(GbSpringUtils.GB_REQUEST_JSON_ERROR_INFO)
//

9.6. 自动requestmap映射

系统提供GbController和GbRestController两个注解

提供@GbController注解为controller类的自动RequestMapping映射,从而使的系统开发人员不必再手工设置RequestMapping和指定view视图的名称。

自动扫描public方法,生成RequestMapping。返回值为void 的方法会自动映射到页面,返回值为String的方法依据返回字符串映射页面,如"redirect:/login/auth"
使用@ResponseBody注解返回json格式数据
可与spring的@Controller和@RequestMapping注解混合使用

9.7. 单表CRUD(Create/Read/Update/Delete) 操作

默认的CRUD结构

action name view name 描述

index

index.html

列表首页

json

返回表格json数据

create

create.html

创建页面

save

保存处理返回json数据

edit

edit.html

修改页面

update

修改处理返回json数据

show

show.html

展示页面

detele

单条删除处理返回json数据

deteles

多条删除处理返回json数据

download

下载excel字节流

9.8. errorController

默认错误处理为ErrorController和error目录下的404和500两个页面

application.yml配置:

server.error.include-stacktrace: NEVER # NEVER , ALWAYS,ON_TRACE_PARAM
server.error.pageforstatus: false   #false时,只有404和500两个页面,设置为true,怎每个Httpstatus 都也对于一个页面(403会被springsecurity处理至/login/denied)

10. 内置拦截器Interceptor

在conf目录下可以创建Interceptor拦截器。拦截器添加@Gbnterceptor指示系统启动时,注册此拦截器

    @Gbnterceptor(value = ['/**'],excludes = [])
    @Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)

拦截器的三个方法preHandle、postHandle、afterCompletion会进行面向切面的编程处理。

11. isAjax 属性

默认参数:系统会在request中提供key为isAjax的Attribute,值为boolean类型,用于controller中判断当前是否为ajax访问.

需要客户端的当前访问携带 X-Requested-With = XMLHttpRequest

12. 分页参数

分页处理:系统默认的分页支持类是PageParams,支持四个属性 max , offset, order ,sort

属性 description 类型 默认值

max

每页的条数

int

10

limit

每页的条数(非必须项 和max参数二选一即可)

int

10

offset

当前数据的起始位置

int

0

sort

排序字段

String

id

order

排序顺序

String

desc

13. 参数自动组装

13.1. domain类型参数

系统扩展spring MVC的参数组装功能,提供基于domain类的自动组装,遵循如下原则:

提交表单参数中若没有id参数,则系统自动创建全新的domain对象,并将其余参数自动赋值。
如果提交表单参数中包含id参数,则系统会调用domain类的get(id)方法,获取domain类的数据库实例,并将其余参数自动赋值。
赋值过程中自动忽略version、clob、blob、byte[]类型的字段赋值。如是Date或Time类型的字段,会调用domain类上字段的@DateTimeFormat注解,来实现自动日期赋值。
如果提交表单参数中包含外键的参数,使用 referenceDomain.id的模式,如“baseUser.id”,赋值时,系统会自动调用findById(id)方法获取外键对象实例,赋值为domain对象。
详细参见工程中用户、角色、登录记录等默认实现

13.2. String类型和Object类型参数

对String类型和Object类型参数默认进行赋值

13.2.1. 增加spring mvc的变量替换处理

在application.yml中增加配置

gb:
    mvc:
      translateStringArgument: true
      translateDomainArgument: true

在Interceptor拦截器上添加如下两个方法,spring mvc会自动调用以替换变量中的参数

    //在domain类的值赋值前进行处理,发生在controller类进行domain组装时
    public Object transferRequestParameterValueBeforeDomainResolver(ServletRequest request, String name, Object value){
        return value;
    }

    //发生在controller类进行String 参数组装时
    public String transferRequestStringParameterValueBeforeResolver(ServletRequest request, String name, Object value){
        return value;
    }

14. json 输出

系统默认使用spring MVC内置的jacksonJSON进行json转换输出。

参看json操作

15. 内置乐观锁

系统使用GORM进行数据的对象关系映射ORMAPPING,因此默认会为每一个domain类提供id、version两个内置属性。
id默认是long型的自增主键.可以通过mapping闭包设置为sequence或UUID
version字段是GORM内部维护的乐观锁,当数据发生修改时,version会自动增加1,系统使用它来判断是否发生了数据脏读,避免脏写。

16. 代码生成器脚手架

系统的代码自动生成工具/webconsole/index,会按照模板文件的样式生成代码。模板文件的位置是/src/main/resources/templates/tools/scaffolding目录.

目录中groovypage后缀的文件是controller类和测试类的模板,模板参数分别是:

name description value

domainClass

相应的实体类

packageName

包名

className

首字母大写的类名称

propertyName

首字母小写的类名称

idType

实体类主键的类型字符

值是"long"或"String"

toolVersion

生成工具的版本

gb-1.0.0

constrainedProperties

domain类的Constraints定义,类型是HashMap

classEnAnnotation

domain类的Title注解中的en值,默认使用类名

classZhAnnotation

domain类的Title注解中的zh值,默认使用类名

propertiesEnAnnotation

domain类属性的Title注解中的en值组成的HashMap

propertiesZhAnnotation

domain类属性的Title注解中的zh值组成的HashMap

html后缀的文件是themleaf3的模板文件。模板参数与上相同。

TIP:目前的Controller.groovypage模板目标是尽量简化,所有操作逻辑都集中在一个类中. 实际生产项目中,建议增加Service.groovypage模板,再统一生成代码.

目前支持的代码逻辑:

name description

Controller.groovypage

controller类模板

Service.groovypage

service类模板

Tests.groovypage

测试类模板

Spec.groovypage

spock测试类模板

*.groovypage

其他的groovy类模板(可根据情况自己扩展-如job类等)

*.html

themleaf3页面模板

*.vue

vue页面模板

17. 关于应用日志打印

因为groovy默认加载java.lang等基础包 ,可以直接使用println 方法打印信息.

gb的脚手架controller模板改为推荐使用groovy.util.logging.Slf4j进行日志输出.

@Slf4j  //使用注解标记 类中会自动添加log变量

//使用
log.error(e.message);

18. 国际化支持i18n

18.1. I18N properties文件

系统默认支持i8n国际化,要求系统工程的文件编码都是UTF-8。资源文件默认在src/main/resources/i18n/目录下, 名称为messages_${lang}.properties ,如messages_zh_CN.properties

name description 对应浏览器的语言或请求参数lang

messages.properties

默认语言

messages_en.properties

英文

en

messages_zh_CN.properties

中文

zh_CN

domain类在资源文件中的规则如下,d代码生成工具会读取domain的title注解来自动生成资源文件的描述。

name description

${domain name}.label

实体名称

${domain name}.${field name}.label

字段名称

相关配置在applicaton.yml中:

spring.messages.basename: i18n/messages  //具体资源文件的目录位置
spring.messages.cache-seconds: 3600      //资源文件自动加载期间缓存的毫秒数

18.2. 获取i18n的信息

系统会根据访问浏览器默认的语言来判断使用的具体资源文件:

controller或service中获取: GbSpringUtils.getI18nMessage("companyBusiness.label");

18.3. 错误处理获取i18n提示信息

GORM实例的save方法 返回boolean值,为false时,obj.errors.allErrors 是错误的集合(obj指GORM实例对象), 每个错误是是org.springframework.validation.FieldError 类型的实例, 默认四个参数 error.code,error.arguments,error.defaultMessage,locale, 其中的locale是读取浏览器的内容-》语言设置

register注册页面和controller类进行了自定义的示例

19. 关于数据库schema的自动同步权限

需要具有相关schema的表、索引、约束等对象的创建权限

若数据库为oracle,需要数据库中有名为hibernate_sequence的sequence对象

20. GORM进行ORMAPPING

20.1. GORM的domain类定义

基本定义语法
Entity定义
属性
约束 (20多个内置约束)
映射  (20多中映射规则)
编译期会自动扩展64个方法

20.2. 默认的属性

Id
Version  处理脏读脏写 (底层维护)
//可选的timestamp
dateCreated
lastUpdated

20.3. api及参考文档

相关的参照文档:GORM数据操作文档

20.4. event

可定制的注入事件 onLoad 对象从数据库中加载时触发 beforeInsert 数据插入前触发 (返回false,终止数据插入) beforeUpdate 数据修改前触发 (返回false,终止数据修改) beforeDelete 数据删除前触发 (返回false,终止数据删除) beforeValidate 数据约束校验前触发 afterload 对象加载后触发 afterInsert 数据插入后触发 afterUpdate 数据修改后触发 afterDelete 数据删除后触发

class Person {
        String name
        String password
        static constraints = {
                name(size: 5..45);
        }
        def  beforeValidate(List propertiesBeingValidated) {
                name = name?.trim()
          }
        def beforeInsert() {
                encodePassword()
         }
        def beforeUpdate() {
           if (isDirty('password')) {
                encodePassword()
           }
        }
        static mapping = {
           password (column: '`password`')
       }
}

20.5. 设计技巧

20.5.1. oneToOne 1对1

class Face {
    Nose nose
    static hasOne = [nose: Nose]
        static constraints = {
                nose(nullable:false,unique:true)
        }
}
class Nose {
    Face face
}

20.5.2. oneToMany 1对多

class Author {

    String name

    static hasMany = [books: Book]
}

class Book {
    static belongsTo = [author: Author]
    String title
}

20.5.3. manyToMany 多对多

多对多的设置时,要求两个类都设置静态的hasMany属性。并且从属方一定设置静态的belongsTo属性,指明隶属于主方的关系。

class Book {
    static belongsTo = Author
    static hasMany = [authors:Author]
    String title
}

class Author {
    static hasMany = [books:Book]
    String name
}

20.5.4. mappedBy

当一对多的映射有多个,并且关联的都是一个domain类时,要配置mappedBy。

class Airport {

    static mappedBy = [outgoingFlights: 'departureAirport',
                       incomingFlights: 'destinationAirport']

    static hasMany = [outgoingFlights: Route,
                      incomingFlights: Route]
}
class Route {
    Airport departureAirport
    Airport destinationAirport
}
进一步学习Eager and Lazy Fetching 和级联操作配置,可参看Groovy-ORM官方文档

20.6. oracle的特殊情况

20.6.1. 使用sequnce替代其他数据库的自增主键

domain类的主键id默认是自增主键,可通过如下方式改为字符串的uuid

class Route {
    String id;
    .....
    ...
    static mapping = {
            id generator:'uuid'
    }

}

也可如下方式使用sequence

class Route {
    .....
    ...
    static mapping = {
        id (generator: 'org.hibernate.id.enhanced.SequenceStyleGenerator',params:[sequence_name:'Route_id_seq'])
    }

}

20.6.2. 文件byte[]的存储

关于文件的存储有几种普遍作法

存储在文件系统中,数据库中存储文件路径

存在操作系统文件目录的管理限制,并且需要专门的数据备份策略。

存储在数据库的二进制字段中

如mysql、sqlserver、oracle等数据库可直接使用byte[]字段存储

class Attachment {
    @Title(zh_CN = '文件名')
    String name
    @Title(zh_CN = '数据')
    byte[] data
    @Title(zh_CN = '创建日期')
    Date dateCreated
    @Title(zh_CN = '修改日期')
    Date lastUpdated
    static constraints = {
        fileName(size:0..500,blank: true,nullable:true)
        data(nullable:true,size:(0..1024*1024*40));
    }

    String toString(){
        return fileName
    }
    static mapping = {

    }
}

在Controller中赋值操作如下:

public String upload(org.springframework.web.multipart.MultipartHttpServletRequest request,Model model){
        MultipartFile file = request.getFile('attachmentFile');
        if(file ||!file?.empty) {
            Attachment attachment=new Attachment();
            attachment.name=file.originalFilename;
            attachment.data=file.getBytes();
            attachment.save(flush:true);
        }
    }

因为oracle中存在单表只允许一个long型字段的限制(ORA-01754 表只能包含一个LONG类型的列),因此当遇到多个byte[]类型字段时,以上方法将不适用,要采用BLOB类型处理。

import java.sql.Blob;
class Attachment {
    @Title(zh_CN = '文件名')
    String name
    @Title(zh_CN = '数据')
    Blob data
    @Title(zh_CN = '创建日期')
    Date dateCreated
    @Title(zh_CN = '修改日期')
    Date lastUpdated
    static constraints = {
        fileName(size:0..500,blank: true,nullable:true)
        data(nullable:true,size:(0..1024*1024*40));
    }

    String toString(){
        return fileName
    }
    static mapping = {
        data type:'blob'
    }
}

在Controller中赋值操作如下:

public String upload(org.springframework.web.multipart.MultipartHttpServletRequest request,Model model){
        MultipartFile file = request.getFile('attachmentFile');
        if(file ||!file?.empty) {
            Attachment attachment=new Attachment();
            attachment.name=file.originalFilename;
            attachment.data=new javax.sql.rowset.serial.SerialBlob(file.getBytes());
            attachment.save(flush:true);
        }
    }

获取字节数据时的代码如下:

   Attachment attachment= Attachment.get(1L);
   Byte[] data=attachment?.data?.binaryStream?.bytes;
存储进mongdb的gridfs中

适合海量二进制小文件的存储

存储进分布式文件系统,使用minIO块存储

20.6.3. 关于动态修改dynamicUpdate的使用

尽管GORM默认使用version字段提供乐观锁防止脏读脏写,但为应付高并发修改或提供性能,同样需要只更新部分字段的动态更新功能。

GORM并不支持Hibernate的DynamicUpdate 注解,而是在mapping闭包中有同样功能的dynamicUpdate方法。

        static mapping = {
                dynamicUpdate true
        }

在domain类中添加后,可以看到save方法的输出的sql日志只会影响到修改的字段。(注意BaseUser类不可使用dynamicUpdate)

21. 关于docker中部署的springsecurity ACL更新

因为项目会默认加载springsecurity和 jwt rest,并在内存缓存requestmap数据进行安全防护,在requestmap表变更时,自动刷新内存的缓存。 这样的方式,在单体项目中没有问题,可是如果集群部署或docker容器自动伸缩性部署,就面临无法及时更新的问题。 解决方案如下:

1. 项目中集成 redis 或 AMQP
2. 在RequestmapController中添加主题订阅代码,更新内存
3. 在RequestmapController中save和update代码中的发布主题内容

这样在多个容器部署时,能自动同步更新操作,并刷新内存,同步requestmap的防护内容。