Amazon API Gateway 踩坑记

最近项目中用到了Amazon API Gateway,当中遇到了不少的坑。本文主要罗列了在使用amazon api gateway中遇的问题,希望能帮助到遇到相同问题的开发者。

不支持parmeters array

1
GET /user/1/addresses?addressId=1&addressId=2

服务端接收到的只有一个值,解决办法passing-array-query-parameters-with-api-gateway-to-lambda

POST/PUT 返回 406

当使用json body发送put/post请求时,api gateway有一定机率返回406,查了半天也找出什么原因,后来在stackoverflow上找到了答案,需要在Integration Request中添加Accept头,并且设置其值为空字符串。具体什么原因估计只有AWS api gateway的开发者才知道了。
aws-api-gateway-returns-http-406

URL Query String Parameter、header需要显示设定

如果不在Method Request里显示声名URL Query String Parameter的话,请求参数是不会转发到后台的,request header同理。

Grails参数绑定不支持application/x-amz-json-1.0/application/x-amz-json-1.0 content type.

请求转发到后台时,API gateway会将application/json转换成application/x-amz-json-1.0/application/x-amz-json-1.0,需要修改代码,添加对这两个header的支持,不然会无法绑定参数。

Java 浮点数存储方式

Java浮点数使用的是IEEE二进制浮点数算术标准。

浮点数分为符号,指数,分数三部分。以32位浮点数为例,分为1符号位+8指数位+23分数位。

1(bit) 8(bit) 23(bit)
符号 指数 分数

以173.7为例:

173.7为正数,故符号位为0

0 00000000 00000000000000000000000

将其转换成1.??? * 2^?形式

1
2
3
4
5
6
7
173.7/2=86.85            2^1
86.85/2=43.425 2^2
43.425/2=21.7125 2^3
21.7125/2=10.85625 2^4
10.85625/2=5.428125 2^5
5.428125/2=2.7140625 2^6
2.7140625/2=1.35703125 2^7

最终格式为1.35703125*2^7 ,指数位的值为7,由于指数部分是由Bias偏移量来表示的,float的偏移量为Bias=2^k-1 -1=2^8-1 -1=127,做指数部分的值为127+7=134,134的二进制值为 1000 0110,则符号位+指数为存储格式如下:

0 1000 0110 00000000000000000000000

计算尾数部分,由于浮点数最终都是以1.??? * 2^?的形式保存的,小数点左边永远是1,所以我们只需关心小数点后面部分

Number X2 1/0
0.35703125 0.7140625 0
0.7140625 1.428125 1
0.428125 0.85625 0
0.85625 1.7125 1
0.7125 1.425 1
0.425 0.85 0
0.85 1.7 1
0.7 1.4 1
0.4 0.8 0
0.8 1.6 1
0.6 1.2 1
0.2 0.4 0
0.4 0.8 0

由于到了0.4这一步这边是会个循环,0110 0110 …
加上尾数部分,最终存储格式如下:
0 1000 0110 01011011011001100110011

我们可以通过Float.floatToIntBits(173.7)来验证结果:

1
2
3
Float.floatToIntBits(173.7) = 1127068467
1127068467用二进制表示,就是
0 10000110 01011011011001100110011

Grails 3.2.11 Bugs

  • Functional Testing setup无法回滚
1
2
3
4
5
6
7
8
9
10
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
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
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
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
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
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
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
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是文件系统对大小写处理的差异导制的,所以在开发时要考虑到文件系统对大小写处理不一致的问题。