引言
充分利用node的高吞吐量和java服務(wù)的穩(wěn)定性,特搭建node+java的開發(fā)和運(yùn)行框架。前端代碼采用js,一部分作為界面代碼運(yùn)行于瀏覽器,一部分做為轉(zhuǎn)發(fā)代理運(yùn)行于node。后端java采用spring實(shí)現(xiàn)restful的web service。
從瀏覽器訪問restful service,需要經(jīng)過node轉(zhuǎn)發(fā),轉(zhuǎn)發(fā)請(qǐng)求的代碼采用node的pipe,該方法實(shí)現(xiàn)流間的管道對(duì)接,減少了傳統(tǒng)的從一個(gè)流中讀取內(nèi)容然后寫入目標(biāo)流中的系統(tǒng)開銷。
在實(shí)際的實(shí)現(xiàn)中遇見一個(gè)問題:瀏覽器發(fā)送post請(qǐng)求,經(jīng)過node代理將請(qǐng)求轉(zhuǎn)發(fā)給tomcat下的spring restful api,而spring無法獲取到請(qǐng)求體。
問題描述
瀏覽器通過jquery.ajax發(fā)送post請(qǐng)求,請(qǐng)求一個(gè)restful api,如:/sh/person,請(qǐng)求體為{"name":"test","age":66},
jquery發(fā)送post請(qǐng)求:
var data = {name:"test",age:66};
$.ajax({
type:"post",
url:"/sh/person",
data:JSON.stringify(data),
//processData:false,
success:function(d){
},
dataType:'json',
contentType:"application/json; charset=utf-8"
});
由node7通過pipe將瀏覽器的api請(qǐng)求轉(zhuǎn)發(fā)到后端spring restful web service
代碼:
app.use('/sh/', function(req, res, next) {
var options = {
host: "127.0.0.1",
port: "8080",
path: '/sh'+req.url,
method: req.method,
headers: req.headers
};
var request = http.request(options,function(response){
res.statusCode = response.statusCode;
response.pipe(res);
}).on("error",function(){
res.statusCode = 503;
res.end();
});
req.pipe(request);
// request.write(JSON.stringify({name:"test",age:66}));
// request.end();
});
tomcat8下spring4 restful 控制器的方法
@RequestMapping(value = "/person", method = RequestMethod.POST)
public @ResponseBody
Person addPerson(@RequestBody Person person) {
personService.add(person);
return person;
}
spring在DispatchServlet中解析請(qǐng)求體,報(bào)如下錯(cuò)誤,該錯(cuò)最終是由tomcat讀取請(qǐng)求的編碼信息時(shí)拋出的SocketTimeoutException引起:
org.springframework.http.converter.HttpMessageNotReadableException: Could not read document: null; nested exception is java.net.SocketTimeoutException
at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readJavaType(AbstractJackson2HttpMessageConverter.java:228)
......
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:745)
Caused by: java.net.SocketTimeoutException
at org.apache.tomcat.util.net.NioBlockingSelector.read(NioBlockingSelector.java:202)
at org.apache.tomcat.util.net.NioSelectorPool.read(NioSelectorPool.java:250)
at org.apache.tomcat.util.net.NioSelectorPool.read(NioSelectorPool.java:231)
at org.apache.coyote.http11.InternalNioInputBuffer.fill(InternalNioInputBuffer.java:133)
at org.apache.coyote.http11.InternalNioInputBuffer$SocketInputBuffer.doRead(InternalNioInputBuffer.java:177)
at org.apache.coyote.http11.filters.IdentityInputFilter.doRead(IdentityInputFilter.java:110)
at org.apache.coyote.http11.AbstractInputBuffer.doRead(AbstractInputBuffer.java:414)
at org.apache.coyote.Request.doRead(Request.java:476)
at org.apache.catalina.connector.InputBuffer.realReadBytes(InputBuffer.java:350)
at org.apache.tomcat.util.buf.ByteChunk.substract(ByteChunk.java:395)
at org.apache.catalina.connector.InputBuffer.read(InputBuffer.java:375)
at org.apache.catalina.connector.CoyoteInputStream.read(CoyoteInputStream.java:190)
at com.fasterxml.jackson.core.json.ByteSourceJsonBootstrapper.ensureLoaded(ByteSourceJsonBootstrapper.java:489)
at
......
排查
不通過node的pipe轉(zhuǎn)發(fā),直接發(fā)送post請(qǐng)求是沒有問題的,如通過postman工具直接發(fā)送post請(qǐng)求是沒有問題的,且node的轉(zhuǎn)發(fā)實(shí)現(xiàn)不去pipe請(qǐng)求流,調(diào)整為直接創(chuàng)建新的請(qǐng)求并重新發(fā)送請(qǐng)求體也是沒有問題的
跟蹤spring源碼發(fā)現(xiàn),發(fā)生問題的測試案例下,查看request的輸入流inputstream.available()為0,運(yùn)行read同樣拋出以上異常。也就是說經(jīng)過node轉(zhuǎn)發(fā)過來的流body體為空。
-
同樣的方式將請(qǐng)求轉(zhuǎn)發(fā)到一個(gè)jetty server里,讀取輸入流也報(bào)錯(cuò)
org.eclipse.jetty.io.EofException: early EOF at org.eclipse.jetty.server.HttpInput.read(HttpInput.java:65) at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:284) at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:326) at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178) at java.io.InputStreamReader.read(InputStreamReader.java:184)
結(jié)論
通過以上排查確定后端問題的可能性非常小,tomcat和jetty讀取body都會(huì)出錯(cuò)。
那么問題還是在node端。
從后端調(diào)試發(fā)現(xiàn)node轉(zhuǎn)發(fā)post過來的body體為空,說明應(yīng)該是有動(dòng)作已經(jīng)讀取了請(qǐng)求的輸入流,因?yàn)檎?qǐng)求的輸入流只能讀取一次。
最后,發(fā)現(xiàn)node端有如下代碼:
var bodyParser = require('body-parser');
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
該代碼是由express-generator自動(dòng)生成,因此一直沒得到關(guān)注。body-parser用于body體的解析,因此其自動(dòng)讀取了請(qǐng)求輸入流導(dǎo)致瀏覽器請(qǐng)求被pipe到后端后,后端讀取的body體為空。
該代碼用不上,刪除掉即可。