C#以post方式调用struts rest-plugin service的问题

struts2: 玩转 rest-plugin 一文中,学习了用struts2开发restful service的方法,发现用c#以post方式调用时各种报错,但java、ajax,包括firefox 的rest client插件测试也无问题。

 

先给出rest service中的这个方法:

C#以post方式调用struts rest-plugin 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     }
View Code

代码不复杂,post一段String过来(xml/json/html格式均可),自动映射成Order对象的实例model,然后根据请求HttpHeader中的Content-Type,如果是xml(application/xml),则返回model对应的xml,如果是json(application/json),则返回model对应的json,其它则返回页面

 

c#的调用代码:

C#以post方式调用struts rest-plugin service的问题
 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 }
View Code

这二种常用的调用方式,居然全跪了,返回的结果是一堆java异常:
 java.lang.NullPointerException
        at org.apache.struts2.convention.ConventionUnknownHandler.handleUnknownActionMethod(ConventionUnknownHandler.java:423)
        at com.opensymphony.xwork2.DefaultUnknownHandlerManager.handleUnknownMethod(DefaultUnknownHandlerManager.java:96)

...

无奈百度了一圈,发现还有另一种方法,利用TcpClient调用

C#以post方式调用struts rest-plugin service的问题
 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 }
View Code

总算调用成功了,但是由于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()方法,看下源码:

C#以post方式调用struts rest-plugin service的问题
  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     }
View Code

注意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方法,安抚下双方,不让调用出错即可。

 

你可能感兴趣的:(service)