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, , 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. // If a method hasn't been explicitly named, try to guess using ReST-style patterns
  47. if (mapping.getMethod() == null) {
  48.  
  49. if (isOptions(request)) {
  50. mapping.setMethod(optionsMethodName);
  51.  
  52. // Handle uris with no id, possibly ending in '/'
  53. } else if (lastSlashPos == -1 || lastSlashPos == fullName.length() -1) {
  54.  
  55. // Index e.g. foo
  56. if (isGet(request)) {
  57. mapping.setMethod(indexMethodName);
  58.  
  59. // Creating a new entry on POST e.g. foo
  60. } else if (isPost(request)) {
  61. if (isExpectContinue(request)) {
  62. mapping.setMethod(postContinueMethodName);
  63. } else {
  64. mapping.setMethod(postMethodName);
  65. }
  66. }
  67.  
  68. // Handle uris with an id at the end
  69. } else if (id != null) {
  70.  
  71. // Viewing the form to edit an item e.g. foo/1;edit
  72. if (isGet(request) && id.endsWith(";edit")) {
  73. id = id.substring(0, id.length() - ";edit".length());
  74. mapping.setMethod(editMethodName);
  75.  
  76. // Viewing the form to create a new item e.g. foo/new
  77. } else if (isGet(request) && "new".equals(id)) {
  78. mapping.setMethod(newMethodName);
  79.  
  80. // Removing an item e.g. foo/1
  81. } else if (isDelete(request)) {
  82. mapping.setMethod(deleteMethodName);
  83.  
  84. // Viewing an item e.g. foo/1
  85. } else if (isGet(request)) {
  86. mapping.setMethod(getMethodName);
  87.  
  88. // Updating an item e.g. foo/1
  89. } else if (isPut(request)) {
  90. if (isExpectContinue(request)) {
  91. mapping.setMethod(putContinueMethodName);
  92. } else {
  93. mapping.setMethod(putMethodName);
  94. }
  95. }
  96. }
  97. }
  98.  
  99. // cut off the id parameter, even if a method is specified
  100. if (id != null) {
  101. if (!"new".equals(id)) {
  102. if (mapping.getParams() == null) {
  103. mapping.setParams(new HashMap());
  104. }
  105. mapping.getParams().put(idParameterName, new String[]{id});
  106. }
  107. fullName = fullName.substring(0, lastSlashPos);
  108. }
  109.  
  110. mapping.setName(fullName);
  111. return mapping;
  112. }
  113. // if action name isn't specified, it can be a normal request, to static resource, return null to allow handle that case
  114. return null;
  115. }

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

C#以post方式调用struts rest-plugin service的问题的更多相关文章

  1. 用HTTP方式调用gearman任务处理

    本来以为是个挺美好的东西,结果... 这样的方式非常不安全,尤其是假设暴露在公网地址,非常easy被攻击,并且gearman的http服务远没有专业的webserver健壮. 攻击方式非常easy:t ...

  2. struts2 2.5.16 通配符方式调用action中的方法报404

    1.问题描述 在struts.xml中配置用通配符方式调用action中的add()方法,访问 http://localhost:8080/Struts2Demo/helloworld_add.act ...

  3. Atitit 动态调用webservice与客户端代理方式调用

    Atitit 动态调用webservice与客户端代理方式调用 方式1: 使用call.invoke  直接调用WSDL,缺点:麻烦,不推荐--特别是JAVA调用.NET的WS时,会有不少的问题需要解 ...

  4. YbSoftwareFactory 代码生成插件【二十五】:Razor视图中以全局方式调用后台方法输出页面代码的三种方法

    上一篇介绍了 MVC中实现动态自定义路由 的实现,本篇将介绍Razor视图中以全局方式调用后台方法输出页面代码的三种方法. 框架最新的升级实现了一个页面部件功能,其实就是通过后台方法查询数据库内容,把 ...

  5. HttpClient Get/Post方式调用Http接口

    本节摘要:本节主要分别介绍如何用get方式.post方式向http接口发送数据. preparation 1. 项目环境如下: myeclipse6.5 .tomcat5.0.system:xp.JD ...

  6. 反射-优化及程序集等(用委托的方式调用需要反射调用的方法(或者属性、字段),而不去使用Invoke方法)

    反射-优化及程序集等(用委托的方式调用需要反射调用的方法(或者属性.字段),而不去使用Invoke方法)   创建Delegate (1).Delegate.CreateDelegate(Type, ...

  7. Matlab与C/C++联合编程之Matlab以MEX方式调用C代码(五)完整过程加示

    如下为本人亲证代码: 一: 编译器的安装与配置(环境不同,显示结果不同) 要使用MATLAB编译器,用户计算机上应用事先安装与MATLAB适配的以下任何一种ANSI C/C++编译器: 5.0.6.0 ...

  8. JS方式调用本地的可执行文件

    看到一个方法,有些用,先存下来,有用的时候再用. 前几天,在IE,FIREFOX中实现了用JS方式调用本地的可执行文件.地址:www.yihaomen.com/article/js/211.htm , ...

  9. AutoCAD.NET 不使用P/Invoke方式调用acad.exe或accore.dll中的接口(如acedCommand、acedPostCommand等)

    使用C#进行AutoCAD二次开发,有时候由于C#接口不够完善,或者低版本AutoCAD中的接口缺少,有些工作不能直接通过C#接口来实现,所以需要通过P/Invoke的方式调用AutoCAD的其他DL ...

随机推荐

  1. Web API在OWIN下实现OAuth

    OAuth(Open Authorization) 为用户资源的授权提供了一个安全的.开放而又简易的标准.与以往的授权方式不同之处是OAuth的授权不会使第三方触及到用户的帐号信息(如用户名与密码), ...

  2. Git从零教你入门(4):Git服务之 gogs部署安装

    Git从零入门系列4: 先看上一篇文章: http://www.51testing.com/index.php?uid-497177-action-viewspace-itemid-3706817 今 ...

  3. php配置php-fpm启动参数及配置详解

    约定几个目录 /usr/local/php/sbin/php-fpm/usr/local/php/etc/php-fpm.conf/usr/local/php/etc/php.ini一,php-fpm ...

  4. 【CSharp】C#中equals与==小记

        序:        昨天技术群中的一个小伙伴发了几个字符串以及值类型比较的面试题,没想到我们的答案不尽人意...下面是截图以及答案,看看与各位看官的答案是否相同.                ...

  5. Cloudera5.8.3:Flume+Morphline+Solr开发小技巧

    1.Flume和Morphline添加日志打印 log4j.logger.org.apache.flume.sink.solr=DEBUG log4j.logger.org.kitesdk.morph ...

  6. MySQL 5.6 主从复制如何处理——触发器,函数,存储过程,调度事件

      截图来自MySQL5.6的pdf版文档. 说明: 1)基于语句的复制时,trigger会在slave上执行,所以slave上也需要有trigger的定义,不然会导致主从数据不一致的: 2)基于行的 ...

  7. Mysql常用的一些技巧命令

    1.统计指定数据库下表的数量 mysql > use information_schema; mysql > SELECT count(TABLE_NAME) FROM informati ...

  8. Servlet/JSP-03 HttpServlet

    一. GenericServlet GenericServlet本身是一个抽象类,并且实现了Servlet和ServletConfig接口 其在内部定义了一个私有的ServletConfig类型的变量 ...

  9. 使用SQL语句创建SQL数据脚本(应对万网主机部分不支持导出备份数据)

    1.查询待导出表Ad中的数据. SELECT * FROM [DB_Temp].[dbo].[Ad] 2.编写存储过程. --将表数据生成SQL脚本的存储过程 CREATE PROCEDURE dbo ...

  10. my_strcat()

    char* my_strcat(char* S1,const char* S2){ //严格符合strcat()的接口形式,需要的S1空间是两个字符串空间总和-1. int i=0,j=0; whil ...