Grails 3.2.11 Bugs

  • Functional Testing setup无法回滚
1
2
3
4
5
6
7
8
9
10
11
12
class BaseFunctionalTest extends GebSpec {
def setup() {
Dog dog = new Dog(name:'xxxx')
}

//需要手动删除
def cleanup() {
Dog.findByName('xxxx')?.delete()
}
}


  • JSON Views循环渲染bug

如果多个GORM实体存在循环引用,则会产生stackover flow异常,如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
Class A {
static hasMany = [children: B]
}

Class B {
A parent
}

def a = new A()
def b = new B(parent:a)
a.addToChildren(b)

json g.render(a, [deep: true])
  • isDirty方法实际上时通过检测类属性指向的引用是否改变来实现的,假如是属性内部的属性发生改变,isDirty方法实际是检测不到的。如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Person{
String firstName
String lastName
Address address
}

class Address{
String city
String street
}

Address address = new Address('shanghai','street')
Person person = new Person('fname','lname',address)

person.save()
person.firstName = 'xxxx'
person.isDirty() == true
person.isDirty('firstName') == true

person.save()
address.city = 'beijing'
person.isDirty('address') == false
person.isDirty() == false

person.address.isDirty() == true
person.address.isDirty('city') == true
  • build test data plugin 不支持自定义约束的属性,就算在TestDataConfig.groovy中设置了也没用,如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
testDataConfig {
sampleData {
Dog {
name = { ->
UUID.randomUUID().toString().toUpperCase().replace('-', '')
}
}
}
}

Class Dog {
String owner
String name

static constraints = {
name validator: { val, obj ->
...
}
}
}

Dog dog = Dog.build()
dog.name == 'name'
  • one-to-many关联中,用left join的方式从many的一方查找出来one,one关联的many对象不正确,如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Person {
static hasMany = [dogs:Dog]
}

class Dog {
String name
}

Dog a = new Dog('A')
Dog b = new Dog('B')

Person p = new Person('P')
person.addToDogs(a)
person.addToDogs(b)
person.save()

Collection<Person> result = Person.withCriteria {
createAlias('dogs', 'd', org.hibernate.sql.JoinType.LEFT_OUTER_JOIN)
eq('d.name', 'A')
}

result[0].dogs.size() == 1

result = Person.withCriteria {
dogs {
eq('name','A')
}
}

result[0].dogs.size() == 2

Spring RestTemplate 打印Request及Response内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import org.apache.commons.io.IOUtils
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.http.HttpRequest
import org.springframework.http.client.ClientHttpRequestExecution
import org.springframework.http.client.ClientHttpRequestInterceptor
import org.springframework.http.client.ClientHttpResponse


class LoggingRequestInterceptor implements ClientHttpRequestInterceptor {

static final String DEFAULT_ENCODING = 'UTF-8'
static final Logger LOGGER = LoggerFactory.getLogger(LoggingRequestInterceptor)

@Override
ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
traceRequest(request, body)
ClientHttpResponse response = execution.execute(request, body)
traceResponse(response)
return response
}

private void traceRequest(HttpRequest request, byte[] body) throws IOException {
LOGGER.info('===========================request begin================================================')
LOGGER.info('URI : {}', request.URI)
LOGGER.info('Method : {}', request.method)
LOGGER.info('Headers : {}', request.headers)
LOGGER.info('Request body: {}', new String(body, 'UTF-8'))
LOGGER.info('==========================request end================================================')
}

private void traceResponse(ClientHttpResponse response) throws IOException {
LOGGER.info('============================response begin==========================================')
LOGGER.info('Status code : {}', response.statusCode)
LOGGER.info('Status text : {}', response.statusText)
LOGGER.info('Headers : {}', response.headers)
LOGGER.info('Response body: {}', IOUtils.toString(response.body, DEFAULT_ENCODING))
LOGGER.info('=======================response end=================================================')
}

}

  • 配置RestTemplate
1
2
3
RestTemplate restTemplate = new RestTemplate()
restTemplate.setRequestFactory(new BufferingClientHttpRequestFactory(new SimpleClientHttpRequestFactory()))
restTemplate.setInterceptors([new LoggingRequestInterceptor()])

由于会多次读取request及response body,所以我们这里会使用BufferingClientHttpRequestFactory类来保证能够读取多次。

Groovy正则表达式用法

Matcher example:

1
2
3
4
5
6
7
8
9
10
11
//Matcher example
String regexStr = /gr.*/
String str = 'groovy'

Matcher matcher0 = (str =~ regexStr)
boolean result0 = (str ==~ regexStr)
assert matcher0.matches() == result0

Matcher matcher1 = (str =~ /$regexStr/)
boolean result1 = (str ==~ /$regexStr/)
assert matcher1.matches() == result1

Find example:

1
2
3
4
5
6
def cool = /gr\w{4}/  // Start with gr followed by 4 characters.
Matcher matcher2 = ('groovy, java and grails rock!' =~ /$cool/)
assert 2 == matcher2.count
assert 2 == matcher2.size() // Groovy adds size() method.
assert 'groovy' == matcher2[0] // Array-like access to match results.
assert 'grails' == matcher2.getAt(1)

Sl4j+Logback依赖配置

1
2
3
4
5
6
7
8
9
10
def sl4jVersion = '1.7.25'
def logbackVersion = '1.2.3'

compile group: 'ch.qos.logback', name: 'logback-core', version: logbackVersion
compile group: 'ch.qos.logback', name: 'logback-classic', version: logbackVersion
compile group: 'ch.qos.logback', name: 'logback-access', version: logbackVersion
compile group: 'org.slf4j', name: 'slf4j-api', version: sl4jVersion
compile group: 'org.slf4j', name: 'jcl-over-slf4j', version: sl4jVersion
compile group: 'org.slf4j', name: 'log4j-over-slf4j', version: sl4jVersion

Grails JSON Views 教程

Grails 3.2版本中的rest-api profile加入了JSON View插件,JSON View插件主要用于渲染JSON返回内容,类似于GSP,其好处就是将JSON渲染从控制器层移到了视图层,同时JSON View还能定义模板,继承等。

开始使用

创建视图

JSON View视图文件以.gson为扩展名,且文件要放在grails-app/views目录下。
例:person.gson

1
2
3
json.person {
name "bob"
}

返回的JSON值为:

1
{"person":{"name":"bob"}}

创建模版

模版文件需要以_开头,比如你有一个类名叫QueryResult,则其模版文件完整路径为grails-app/views/queryResult/_queryResult.gson
例:Author.groovy

1
2
3
class Author {
String name
}

模版:grails-app/views/author/_author.gson

1
2
3
4
5
6
model {
Author author
}
json {
name author.name
}

也可以简写为:

1
2
3
4
5

@Field Author author
json {
name author.name
}

如果Author类是Domain Object的话,则模版文件可以写成:

1
2
@Field Author author
json g.render(author)

高级用法

自定义字段

1
2
3
4
5
6
7
8
9
model {
Book book
}

json g.render(book, [deep: false, renderNulls: true]) {
authorName book.author?.name
publishDate new Date().time
}

集合渲染

例1:

1
2
3
4
5
6
7
8
model {
Author author
}

json {
name author.name
books g.render(template: '/book/book', collection: author.books, var: 'book')
}

/book/book引用的是grails-app/views/book目录下名为_book.gson的模版文件

例2:
如果返回结果是个集合,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class AuthorController {
static responseFormats = ['json', 'xml']

def index() {
/**
* 这里需要注意res的类型,
* 如果是List类型,则json view中的model名称则为authorList
* 如果是Set类型,则json view中的model名称则为authorSet
*/
def res = Author.list()
respond(res, [view: 'authorList'])
}
}

  • 这里要注意下res的类型

    • 如果是List类型,则json view中的model名称则为authorList
    • 如果是Set类型,则json view中的model名称则为authorSet
  • Grails默认会在grails-app/views/author/路径下查找authorList.gson视图文件

  • 如果需要在其他Controller下引用该视图的话,需要写绝对路径/author/authorList

_author.gson

1
2
3
4
5
6
7
8
9
model {
Author author
}

json {
name author.name
books g.render(template: '/book/book', collection: author.books, var: 'book')
}

authorList.gson

1
2
3
4
5
6
model {
Collection<Author> authorList = []
}

json tmpl.author(authorList)

authorList要指定下默认值[],如果不指定的话会返回[null]这种JSON

渲染参数

你可以通过includesexcludes参数,来包含或排除一些字段,如

1
2
@Field Author author
json g.render(author,[includes:['title'])

你也可以自定义额外输出的内容:

1
2
3
json g.render(author) {
age 30
}

在默认情况下,JSON View不会返回值为null的字段,如:

1
2
3
4
5
6
7
8
9
10
class Author {
String name
Integer age
}

@Field Author author
json g.render(author)

def a = new Author(name: 'charles',age: 12) => {name:'chalres',age: 12}
def b = new Author(name: null,age: 29)=> {age: 29}

这时候你可以设直renderNulls来返回空值字段:

1
2
@Field Author author
json g.render(author,[renderNulls: true])

则b的返回中就变成:

1
2
def b = new Author(name: null,age: 29)=> {name: null,age: 29}

完整的例子

https://github.com/wancaibida/grails-json-views-example

总结

JSON View用下来还是遇到了不少的问题,主要原因是其约定太多且官方的文档也说的不是很清楚,有 些地方还是要一步步调试来研究源代码,但总的来说比传统的在代码中指定JSON格式方便了不少,用好了能节约不少时间与工作量。

参考链接

https://github.com/skyboy101/widget-store-rest-api
http://views.grails.org/1.1.x/

记一个Grails JSON Views的Bug

最近项目用到了3.2版本的Grails,这个版本中引入了一个新特性JSON views,主要作用是将JSON返回内容视为一种视图,类似于GSP。其好处就是可以在视图层定义返回json的格式,而且可以定义bean的JSON模版,比较灵活。

项目中有一个名为QueryResult的类,并设置了QueryResult类的模版,名为_queryresult.gson

文件目录如下:

1
2
3
4
5
6
bean
package...
QueryResult.groovy
views
queryResult
_queryresult.gson

Controller代码:

1
2
3
def list(xxx){
new QueryResult(xxx)
}

项目在本地开发时没有什么问题,但部署到生产环境时,这个方法返回内空始终为空。

项目在本地开发时是在内置的tomcat运行的,而在生产环境中项目是打包成war文件部署在Linux下的Tomcat。猜测问题可能是项目运行方式不同导致。

调试后发现了如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
WritableScriptTemplate template
if (Environment.isDevelopmentEnvironmentAvailable()) {
template = attemptResolvePath(path)
if (template == null) {
template = attemptResolveClass(path)
}
} else {
template = attemptResolveClass(path)
if (template == null) {
template = attemptResolvePath(path)
}
}
if (template == null) {
template = NULL_ENTRY
}

代码的基本意思是,如果是在开发环境下,模版内容优先通过文件路径来查找,如果没有找到则通过类名来查找,而在生产环境正好反过来。

模版文件_queryresult.gson编译后会生成名为xxx_queryResult__queryresult_gson.class的java类。

JSON Views插件会通过一系列约定的命名方式来找所需要的视图,最终会查找名为xxx_queryResult__queryResult_gson.class的java类(注意:第二个queryResult中的R是大写的),前面说到开发环境下会先通过文件名称来查找,即

1
2
3
4
5
//实际文件名为:xxx_queryResult__queryresult_gson.class
def templateFile = new File('xxx_queryResult__queryResult_gson.class')
if(templateFile.exist()){
return templateFile;
}

,如果文件存在,则返回。可实际的class文件名为xxx_queryResult__queryresult_gson.class,但我开发环境的文件系统是大小写不敏感的!!,所以对于系统来说xxx_queryResult__queryresult_gson.classxxx_queryResult__queryResult_gson.class是同样的文件名,exist方法最终是通过系统的API来寻找文件的,所以templateFile.exist()这句返回结果为真。

在生产环境是下是优先通过类名来查找的,要查找的类名为xxx_queryResult__queryResult_gson.class,而实际的类名为xxx_queryResult__queryresult_gson.class,而java类名是区分大小写的,所以会导致类找不到。

一旦通过类名查找不成功,则会通过文件路径来查,而Linux的文件系统是大小写敏感的,所以会无法找到名为xxx_queryResult__queryResult_gson.class的文件。

最终把模版文件名重命名为_queryResult.gson,接口就运行正常了。

总结:
严格来说这个BUG是文件系统对大小写处理的差异导制的,所以在开发时要考虑到文件系统对大小写处理不一致的问题。

Intellij远程调试Tomcat

  1. 在Tomcat下新建setenv.sh文件

  2. 运行chmod 755 setenv.sh

  3. 将下面的代码添加到setenv.sh文件中

    1
    2
    export CATALINA_OPTS="-agentlib:jdwp=transport=dt_socket,address=1043,server=y,suspend=n"

  4. Intellij Run/Debug Configurations下新建Tomcat Server Remote
    ![Screen Shot 2017-12-02 at 13.53.42](http://static.w2x.me/2017-12-02-Screen Shot 2017-12-02 at 13.53.42.png)

  5. 在新建的页面先择Starup/Connection标签,然后选择Debug选项
    ![Screen Shot 2017-12-02 at 13.56.20](http://static.w2x.me/2017-12-02-Screen Shot 2017-12-02 at 13.56.20.png)

  6. Port的值修改为1043
    ![Screen Shot 2017-12-02 at 13.58.42](http://static.w2x.me/2017-12-02-Screen Shot 2017-12-02 at 13.58.42.png)

  7. Tomcat启动后,Intellij即可远程Debug

Kubernetes入门

Kubernetes入门

Kubernetes是什么

Kubernetes是用来自动化部署,扩展,管理容器应用的工具.

Kubernetes架构

Kubernetes主要由Master和Node两部分组成.
Architecture

Master

Kubernetes集群包含至少一个Master节点和多个Node节点.Master节点主要用来暴露API,调度部署和管理整个集群.

Node

Node是工作节点,Node可能是物理机器也可能是虚拟机,每个节点上都提供了容器的运行环境,比如说Docker,rkt.

Kubernetes对象

Kubernetes对象持久化在kubernetes系统中,Kubernetes用这些对象来描述集群的状态,比如:

  • 部署的应用
  • 应用的行为,比如重启,升级策略
  • 应用程序可用的资源

描述Kubernetes对象

我们通常会通过.yaml来描述Kuberntes对象,Kubernetes客户端会将yaml文件转成JSON来调用API.
例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
apiVersion: apps/v1beta1
kind: Deployment
metadata:
name: nginx-deployment
spec:
replicas: 3
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.7.9
ports:
- containerPort: 80

我们可以通过下面的命令来创建Kubernetes对象:

1
kubectl create -f docs/user-guide/nginx-deployment.yaml --record

字段说明

在yaml文件中,你需要声明以下字段:

  • apiVersion - 调用的API版本
  • kind - 对象类型
  • metadata - 用于查找对象的字段,比如名称,UID,命名空间

一些常见的kubernetes对象有:

  • Pod
  • Deployments
  • Service
  • Ingress

Pod对象

Pod对象是最小的布署单元,包含了一个或多个容器.通常我们不会单独使用Pod.
Controller对象可创建多个Pod,常风的Controller对象有:

  • Deployment
  • StatefulSet
  • DaemonSet

Deployments对象

Deployments对象可以创建Pod,在Deployments对象中你只需要描述你需要的状态,Deployments对象会将当前状态改变成你所需要的状态.

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
apiVersion: apps/v1beta1
kind: Deployment
metadata:
name: nginx-deployment
spec:
replicas: 3
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.7.9
ports:
- containerPort: 80

上面的描述文件创建了3个运行Nginx的Pod对象.

通过下面命令来部署和查看Deployments对象:

1
2
kubectl create -f docs/user-guide/nginx-deployment.yaml --record
kubectl get deployment

Service对象

Service对象将Pod对象抽像成一种服务,并定义了访问这些Pod对象的策略,类似于负载均衡(Loadbalancer).Service对象会通过选择器来选择Pod对象.

例:

1
2
3
4
5
6
7
8
9
10
11
kind: Service
apiVersion: v1
metadata:
name: my-service
spec:
selector:
app: MyApp
ports:
- protocol: TCP
port: 80
targetPort: 9376

Ingress对象

Ingress对象定义了外部请求如何转发到Service,类似于反向代理.
例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: test
annotations:
ingress.kubernetes.io/rewrite-target: /
spec:
rules:
- host: foo.bar.com
http:
paths:
- path: /foo
backend:
serviceName: s1
servicePort: 80
- path: /bar
backend:
serviceName: s2
servicePort: 80

上面的描述文件定义了Service的访问规则,foo上下文路径会被转发到名为s1的Service,bar上下文路径转发到名为s2的Service.

简而言之:

  • Deployments定义了怎样部署应用(应用image,部署多少实例,启动策略等);
  • Service定义了如何抽像Deployments为服务,类似于负载均衡;
  • Ingress定义了请求如何转发到Service,类似反向代理,如nginx.