本文简单介绍了体系的开发规范。
如果你拥有丰富的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 |
---|---|---|
|
相应的实体类 |
|
|
包名 |
|
|
首字母大写的类名称 |
|
|
首字母小写的类名称 |
|
|
实体类主键的类型字符 |
值是"long"或"String" |
|
生成工具的版本 |
gb-1.0.0 |
|
domain类的Constraints定义,类型是HashMap |
|
|
domain类的Title注解中的en值,默认使用类名 |
|
|
domain类的Title注解中的zh值,默认使用类名 |
|
|
domain类属性的Title注解中的en值组成的HashMap |
|
|
domain类属性的Title注解中的zh值组成的HashMap |
html后缀的文件是themleaf3的模板文件。模板参数与上相同。
TIP:目前的Controller.groovypage模板目标是尽量简化,所有操作逻辑都集中在一个类中. 实际生产项目中,建议增加Service.groovypage模板,再统一生成代码.
目前支持的代码逻辑:
name | description |
---|---|
|
controller类模板 |
|
service类模板 |
|
测试类模板 |
|
spock测试类模板 |
|
其他的groovy类模板(可根据情况自己扩展-如job类等) |
|
themleaf3页面模板 |
|
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的防护内容。