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

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

     // POST /orders
public HttpHeaders create() throws IOException, ServletException {
ordersService.doSave(model);
HttpServletResponse response = ServletActionContext.getResponse();
HttpServletRequest request = ServletActionContext.getRequest();
String ContentType = request.getHeader("Content-Type").toLowerCase();
if (ContentType.startsWith("application/xml")) { // 返回xml视图
response.sendRedirect("orders/" + model.getId() + ".xml");
} else if (ContentType.startsWith("application/json")) { // 返回json视图
response.sendRedirect("orders/" + model.getId() + ".json");
} else {// 返回xhtml页面视图
response.sendRedirect("orders/");
}
return null;
}

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

c#的调用代码:

 static string PostDataByWebClient(String postUrl, String paramData, String mediaType)
{
String result = String.Empty;
try
{
byte[] postData = Encoding.UTF8.GetBytes(paramData);
WebClient webClient = new WebClient();
webClient.Headers.Add("Content-Type", mediaType);
byte[] responseData = webClient.UploadData(new Uri(postUrl), "POST", postData);
result = Encoding.UTF8.GetString(responseData);
}
catch (Exception e)
{
Console.WriteLine(e);
result = e.Message;
}
return result;
} static string PostDataByWebRequest(string postUrl, string paramData, String mediaType)
{
string result = string.Empty;
Stream newStream = null;
StreamReader sr = null;
HttpWebResponse response = null;
try
{
byte[] byteArray = Encoding.UTF8.GetBytes(paramData);
HttpWebRequest webReq = (HttpWebRequest)WebRequest.Create(new Uri(postUrl));
webReq.Method = "POST";
webReq.ContentType = mediaType;
webReq.ContentLength = byteArray.Length;
newStream = webReq.GetRequestStream();
newStream.Write(byteArray, , byteArray.Length);
response = (HttpWebResponse)webReq.GetResponse();
sr = new StreamReader(response.GetResponseStream(), Encoding.UTF8);
result = sr.ReadToEnd();
}
catch (Exception ex)
{
Console.WriteLine(ex);
result = ex.Message;
}
finally
{
if (sr != null)
{
sr.Close();
}
if (response != null)
{
response.Close();
}
if (newStream != null)
{
newStream.Close();
}
}
return result;
}

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

...

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

 static string PostDataByTcpClient(string postUrl, string paramData, String mediaType)
{
String result = String.Empty;
TcpClient clientSocket = null;
Stream readStream = null;
try
{
clientSocket = new TcpClient();
Uri URI = new Uri(postUrl);
clientSocket.Connect(URI.Host, URI.Port);
StringBuilder RequestHeaders = new StringBuilder();//用来保存HTML协议头部信息
RequestHeaders.AppendFormat("{0} {1} HTTP/1.1\r\n", "POST", URI.PathAndQuery);
RequestHeaders.AppendFormat("Connection:close\r\n");
RequestHeaders.AppendFormat("Host:{0}:{1}\r\n", URI.Host,URI.Port);
RequestHeaders.AppendFormat("Content-Type:{0}\r\n", mediaType);
RequestHeaders.AppendFormat("\r\n");
RequestHeaders.Append(paramData + "\r\n");
Encoding encoding = Encoding.UTF8;
byte[] request = encoding.GetBytes(RequestHeaders.ToString());
clientSocket.Client.Send(request);
readStream = clientSocket.GetStream();
StreamReader sr = new StreamReader(readStream, Encoding.UTF8);
result = sr.ReadToEnd();
}
catch (Exception e)
{
Console.WriteLine(e);
result = e.Message;
}
finally
{
if (readStream != null)
{
readStream.Close();
}
if (clientSocket != null)
{
clientSocket.Close();
}
}
return result;
}

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

     public ActionMapping getMapping(HttpServletRequest request,
ConfigurationManager configManager) {
ActionMapping mapping = new ActionMapping();
String uri = RequestUtils.getUri(request); uri = dropExtension(uri, mapping);
if (uri == null) {
return null;
} parseNameAndNamespace(uri, mapping, configManager); handleSpecialParameters(request, mapping); if (mapping.getName() == null) {
return null;
} // handle "name!method" convention.
handleDynamicMethodInvocation(mapping, mapping.getName()); String fullName = mapping.getName();
// Only try something if the action name is specified
if (fullName != null && fullName.length() > 0) { // cut off any ;jsessionid= type appendix but allow the rails-like ;edit
int scPos = fullName.indexOf(';');
if (scPos > -1 && !"edit".equals(fullName.substring(scPos + 1))) {
fullName = fullName.substring(0, scPos);
} int lastSlashPos = fullName.lastIndexOf('/');
String id = null;
if (lastSlashPos > -1) { // fun trickery to parse 'actionName/id/methodName' in the case of 'animals/dog/edit'
int prevSlashPos = fullName.lastIndexOf('/', lastSlashPos - 1);
if (prevSlashPos > -1) {
mapping.setMethod(fullName.substring(lastSlashPos + 1));
fullName = fullName.substring(0, lastSlashPos);
lastSlashPos = prevSlashPos;
}
id = fullName.substring(lastSlashPos + 1);
} // If a method hasn't been explicitly named, try to guess using ReST-style patterns
if (mapping.getMethod() == null) { if (isOptions(request)) {
mapping.setMethod(optionsMethodName); // Handle uris with no id, possibly ending in '/'
} else if (lastSlashPos == -1 || lastSlashPos == fullName.length() -1) { // Index e.g. foo
if (isGet(request)) {
mapping.setMethod(indexMethodName); // Creating a new entry on POST e.g. foo
} else if (isPost(request)) {
if (isExpectContinue(request)) {
mapping.setMethod(postContinueMethodName);
} else {
mapping.setMethod(postMethodName);
}
} // Handle uris with an id at the end
} else if (id != null) { // Viewing the form to edit an item e.g. foo/1;edit
if (isGet(request) && id.endsWith(";edit")) {
id = id.substring(0, id.length() - ";edit".length());
mapping.setMethod(editMethodName); // Viewing the form to create a new item e.g. foo/new
} else if (isGet(request) && "new".equals(id)) {
mapping.setMethod(newMethodName); // Removing an item e.g. foo/1
} else if (isDelete(request)) {
mapping.setMethod(deleteMethodName); // Viewing an item e.g. foo/1
} else if (isGet(request)) {
mapping.setMethod(getMethodName); // Updating an item e.g. foo/1
} else if (isPut(request)) {
if (isExpectContinue(request)) {
mapping.setMethod(putContinueMethodName);
} else {
mapping.setMethod(putMethodName);
}
}
}
} // cut off the id parameter, even if a method is specified
if (id != null) {
if (!"new".equals(id)) {
if (mapping.getParams() == null) {
mapping.setParams(new HashMap());
}
mapping.getParams().put(idParameterName, new String[]{id});
}
fullName = fullName.substring(0, lastSlashPos);
} mapping.setName(fullName);
return mapping;
}
// if action name isn't specified, it can be a normal request, to static resource, return null to allow handle that case
return null;
}

注意91-96行,这里有一个判断:

                     }  else if (isPut(request)) {
if (isExpectContinue(request)) {
mapping.setMethod(putContinueMethodName);
} else {
mapping.setMethod(putMethodName);
}
}

再来细看下:isExpectContinue

     protected boolean isExpectContinue(HttpServletRequest request) {
String expect = request.getHeader("Expect");
return (expect != null && expect.toLowerCase().contains("100-continue"));
}

这段代码的意思是如果请求Http头里有Except信息,且等于100-continue,则返回true。如果返回true,刚才那段判断,会返回putContinueMethodName这个变量所指的方法:

  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行为去掉

 webReq.ServicePoint.Expect100Continue = false;//禁止自动添加Except:100-continue到http头信息

这样,最终发出去的头信息,就不会有except行

方案B: Controller中把createContinue方法补上

     public HttpHeaders createContinue() throws IOException, ServletException{
return create();
}

直接调用create方法,安抚下双方,不让调用出错即可。

最新文章

  1. Shell 重定向
  2. SharePoint 2010自定义母版页小技巧——JavaScript和CSS引用
  3. 关于tcpdump抓包一个很详细的介绍
  4. xcode中的一些快捷键
  5. sqlserver使用OpenQuery或OPENROWSET遇到的问题
  6. openstack VM可以ping外部网络,但是外部网络ping不通VM
  7. Java并发实现一(并发的实现之Thread和Runnable的区别)
  8. --------------Hibernate学习(四) 多对一映射 和 一对多映射
  9. Android-第三天
  10. App自动化(2)--Python&Appium实现安卓手机九宫格解锁
  11. java中53个关键字的意义及使用方法
  12. C++客户端访问WebService VS2008
  13. 【English】20190416
  14. python3列表(list)
  15. javascript深入浅出——学习笔记(六种数据类型和隐式转换)
  16. Web3.js 0.20.x API 中文版翻译
  17. 为你的 Hadoop 集群选择合适的硬件
  18. Postman 常用测试结果验证及使用技巧
  19. Linux mysql 5.5.10 二进制安装过程记录和 修改 密码 登录
  20. 移动端触摸滑动插件Swiper使用指南

热门文章

  1. Photo Shop 修改、维护
  2. javascript-简单工厂两种实现方式
  3. 简单的 http 服务器
  4. java 把一个文件夹里图片复制到另一个文件夹里
  5. python scrapy版 极客学院爬虫V2
  6. windows环境下无法引用全局安装的模块问题
  7. ZooKeeper日志与快照文件简单分析
  8. ssh升级
  9. ELF Format 笔记(八)—— 符号的类型和属性(st_info)
  10. HTML 头部标记