struts2: 玩转 rest-plugin 一文中,学习了用struts2开发restful service的方法,发现用c#以post方式调用时各种报错,但java、ajax,包括firefox 的rest client插件测试也无问题。
先给出rest service中的这个方法:
1 // POST /orders
2 public HttpHeaders create() throws IOException, ServletException {
3 ordersService.doSave(model);
4 HttpServletResponse response = ServletActionContext.getResponse();
5 HttpServletRequest request = ServletActionContext.getRequest();
6 String ContentType = request.getHeader("Content-Type").toLowerCase();
7 if (ContentType.startsWith("application/xml")) { // 返回xml视图
8 response.sendRedirect("orders/" + model.getId() + ".xml");
9 } else if (ContentType.startsWith("application/json")) { // 返回json视图
10 response.sendRedirect("orders/" + model.getId() + ".json");
11 } else {// 返回xhtml页面视图
12 response.sendRedirect("orders/");
13 }
14 return null;
15 }
代码不复杂,post一段String过来(xml/json/html格式均可),自动映射成Order对象的实例model,然后根据请求HttpHeader中的Content-Type,如果是xml(application/xml),则返回model对应的xml,如果是json(application/json),则返回model对应的json,其它则返回页面
c#的调用代码:
1 static string PostDataByWebClient(String postUrl, String paramData, String mediaType)
2 {
3 String result = String.Empty;
4 try
5 {
6 byte[] postData = Encoding.UTF8.GetBytes(paramData);
7 WebClient webClient = new WebClient();
8 webClient.Headers.Add("Content-Type", mediaType);
9 byte[] responseData = webClient.UploadData(new Uri(postUrl), "POST", postData);
10 result = Encoding.UTF8.GetString(responseData);
11 }
12 catch (Exception e)
13 {
14 Console.WriteLine(e);
15 result = e.Message;
16 }
17 return result;
18 }
19
20 static string PostDataByWebRequest(string postUrl, string paramData, String mediaType)
21 {
22 string result = string.Empty;
23 Stream newStream = null;
24 StreamReader sr = null;
25 HttpWebResponse response = null;
26 try
27 {
28 byte[] byteArray = Encoding.UTF8.GetBytes(paramData);
29 HttpWebRequest webReq = (HttpWebRequest)WebRequest.Create(new Uri(postUrl));
30 webReq.Method = "POST";
31 webReq.ContentType = mediaType;
32 webReq.ContentLength = byteArray.Length;
33 newStream = webReq.GetRequestStream();
34 newStream.Write(byteArray, 0, byteArray.Length);
35 response = (HttpWebResponse)webReq.GetResponse();
36 sr = new StreamReader(response.GetResponseStream(), Encoding.UTF8);
37 result = sr.ReadToEnd();
38 }
39 catch (Exception ex)
40 {
41 Console.WriteLine(ex);
42 result = ex.Message;
43 }
44 finally
45 {
46 if (sr != null)
47 {
48 sr.Close();
49 }
50 if (response != null)
51 {
52 response.Close();
53 }
54 if (newStream != null)
55 {
56 newStream.Close();
57 }
58 }
59 return result;
60 }
这二种常用的调用方式,居然全跪了,返回的结果是一堆java异常: java.lang.NullPointerException at org.apache.struts2.convention.ConventionUnknownHandler.handleUnknownActionMethod(ConventionUnknownHandler.java:423) at com.opensymphony.xwork2.DefaultUnknownHandlerManager.handleUnknownMethod(DefaultUnknownHandlerManager.java:96)
...
无奈百度了一圈,发现还有另一种方法,利用TcpClient调用
1 static string PostDataByTcpClient(string postUrl, string paramData, String mediaType)
2 {
3 String result = String.Empty;
4 TcpClient clientSocket = null;
5 Stream readStream = null;
6 try
7 {
8 clientSocket = new TcpClient();
9 Uri URI = new Uri(postUrl);
10 clientSocket.Connect(URI.Host, URI.Port);
11 StringBuilder RequestHeaders = new StringBuilder();//用来保存HTML协议头部信息
12 RequestHeaders.AppendFormat("{0} {1} HTTP/1.1\r\n", "POST", URI.PathAndQuery);
13 RequestHeaders.AppendFormat("Connection:close\r\n");
14 RequestHeaders.AppendFormat("Host:{0}:{1}\r\n", URI.Host,URI.Port);
15 RequestHeaders.AppendFormat("Content-Type:{0}\r\n", mediaType);
16 RequestHeaders.AppendFormat("\r\n");
17 RequestHeaders.Append(paramData + "\r\n");
18 Encoding encoding = Encoding.UTF8;
19 byte[] request = encoding.GetBytes(RequestHeaders.ToString());
20 clientSocket.Client.Send(request);
21 readStream = clientSocket.GetStream();
22 StreamReader sr = new StreamReader(readStream, Encoding.UTF8);
23 result = sr.ReadToEnd();
24 }
25 catch (Exception e)
26 {
27 Console.WriteLine(e);
28 result = e.Message;
29 }
30 finally
31 {
32 if (readStream != null)
33 {
34 readStream.Close();
35 }
36 if (clientSocket != null)
37 {
38 clientSocket.Close();
39 }
40 }
41 return result;
42 }
总算调用成功了,但是由于java端是用SendRedirect在客户端重定向的,所以该方法得到的返回结果如下:
HTTP/1.1 302 Found Server: Apache-Coyote/1.1 Location: http://localhost:8080/struts2-rest-ex/rest/orders/230.xml Content-Length: 0 Date: Mon, 27 Oct 2014 03:18:56 GMT Connection: close
是一堆http头的原文,只能曲线救国,将其中的Location:后的部分(即重定向的url),取出来再次get请求。
这样的解决方案显然有点笨拙,继续深挖:
org.apache.struts2.rest.RestActionMapper这个类的getMapping()方法,看下源码:
1 public ActionMapping getMapping(HttpServletRequest request,
2 ConfigurationManager configManager) {
3 ActionMapping mapping = new ActionMapping();
4 String uri = RequestUtils.getUri(request);
5
6 uri = dropExtension(uri, mapping);
7 if (uri == null) {
8 return null;
9 }
10
11 parseNameAndNamespace(uri, mapping, configManager);
12
13 handleSpecialParameters(request, mapping);
14
15 if (mapping.getName() == null) {
16 return null;
17 }
18
19 // handle "name!method" convention.
20 handleDynamicMethodInvocation(mapping, mapping.getName());
21
22 String fullName = mapping.getName();
23 // Only try something if the action name is specified
24 if (fullName != null && fullName.length() > 0) {
25
26 // cut off any ;jsessionid= type appendix but allow the rails-like ;edit
27 int scPos = fullName.indexOf(';');
28 if (scPos > -1 && !"edit".equals(fullName.substring(scPos + 1))) {
29 fullName = fullName.substring(0, scPos);
30 }
31
32 int lastSlashPos = fullName.lastIndexOf('/');
33 String id = null;
34 if (lastSlashPos > -1) {
35
36 // fun trickery to parse 'actionName/id/methodName' in the case of 'animals/dog/edit'
37 int prevSlashPos = fullName.lastIndexOf('/', lastSlashPos - 1);
38 if (prevSlashPos > -1) {
39 mapping.setMethod(fullName.substring(lastSlashPos + 1));
40 fullName = fullName.substring(0, lastSlashPos);
41 lastSlashPos = prevSlashPos;
42 }
43 id = fullName.substring(lastSlashPos + 1);
44 }
45
46
47
48 // If a method hasn't been explicitly named, try to guess using ReST-style patterns
49 if (mapping.getMethod() == null) {
50
51 if (isOptions(request)) {
52 mapping.setMethod(optionsMethodName);
53
54 // Handle uris with no id, possibly ending in '/'
55 } else if (lastSlashPos == -1 || lastSlashPos == fullName.length() -1) {
56
57 // Index e.g. foo
58 if (isGet(request)) {
59 mapping.setMethod(indexMethodName);
60
61 // Creating a new entry on POST e.g. foo
62 } else if (isPost(request)) {
63 if (isExpectContinue(request)) {
64 mapping.setMethod(postContinueMethodName);
65 } else {
66 mapping.setMethod(postMethodName);
67 }
68 }
69
70 // Handle uris with an id at the end
71 } else if (id != null) {
72
73 // Viewing the form to edit an item e.g. foo/1;edit
74 if (isGet(request) && id.endsWith(";edit")) {
75 id = id.substring(0, id.length() - ";edit".length());
76 mapping.setMethod(editMethodName);
77
78 // Viewing the form to create a new item e.g. foo/new
79 } else if (isGet(request) && "new".equals(id)) {
80 mapping.setMethod(newMethodName);
81
82 // Removing an item e.g. foo/1
83 } else if (isDelete(request)) {
84 mapping.setMethod(deleteMethodName);
85
86 // Viewing an item e.g. foo/1
87 } else if (isGet(request)) {
88 mapping.setMethod(getMethodName);
89
90 // Updating an item e.g. foo/1
91 } else if (isPut(request)) {
92 if (isExpectContinue(request)) {
93 mapping.setMethod(putContinueMethodName);
94 } else {
95 mapping.setMethod(putMethodName);
96 }
97 }
98 }
99 }
100
101 // cut off the id parameter, even if a method is specified
102 if (id != null) {
103 if (!"new".equals(id)) {
104 if (mapping.getParams() == null) {
105 mapping.setParams(new HashMap());
106 }
107 mapping.getParams().put(idParameterName, new String[]{id});
108 }
109 fullName = fullName.substring(0, lastSlashPos);
110 }
111
112 mapping.setName(fullName);
113 return mapping;
114 }
115 // if action name isn't specified, it can be a normal request, to static resource, return null to allow handle that case
116 return null;
117 }
注意91-96行,这里有一个判断:
1 } else if (isPut(request)) {
2 if (isExpectContinue(request)) {
3 mapping.setMethod(putContinueMethodName);
4 } else {
5 mapping.setMethod(putMethodName);
6 }
7 }
再来细看下:isExpectContinue
1 protected boolean isExpectContinue(HttpServletRequest request) {
2 String expect = request.getHeader("Expect");
3 return (expect != null && expect.toLowerCase().contains("100-continue"));
4 }
这段代码的意思是如果请求Http头里有Except信息,且等于100-continue,则返回true。如果返回true,刚才那段判断,会返回putContinueMethodName这个变量所指的方法:
1 private String postContinueMethodName = "createContinue";
但是Controller里只有create方法,并没有createContinue方法,所以找不到方法,当然报错。
而c#中如果以post方法请求url时,不论是HttpWebRequest还是WebClient,默认都会添加expect = 100-continue的头信息,因此c#调用时会报错,而firefox的RestClient插件、java调用、ajax调用,因为没有拼except信息,不会出错。
那么except = 100-continue是什么东西呢?为何c#要自动拼这上这行头信息?可以参见园友的文章:http之100-continue,大意是说:
如果客户端向服务端post数据,考虑到post的数据可能很大,搞不好能把服务器玩坏(或者超时),所以,有一个贴心的约定,客户端先发一个except头信息给服务器,问下:我要post数据了,可能很大,你想想要不要收,采用什么措施收?如果服务器很聪明,可能会对这种情况做出特殊响应,就比如刚才的java代码,遇到这种头信息,不是调用create方法,而是createContinue方法。
这本是一个不错的约定,但是偏偏本文中的Controller方法,又没有提供createContinue方法,所以辜负了客户端的美意,好心当成驴肝肺了。
终极解决方案:
方案A:HttpWebRequest请求时,把默认的except行为去掉
1 webReq.ServicePoint.Expect100Continue = false;//禁止自动添加Except:100-continue到http头信息
这样,最终发出去的头信息,就不会有except行
方案B: Controller中把createContinue方法补上
1 public HttpHeaders createContinue() throws IOException, ServletException{
2 return create();
3 }
直接调用create方法,安抚下双方,不让调用出错即可。