标签归档:json

Java自动装箱/拆箱中的坑

Java的装箱/拆箱发生在包装类与其对应的基本类型之间,比如:

1
2
3
int int1 = 1;
Integer integer2 = new Integer(int1); //装箱
int int3 = integer2.intValue(); //拆箱

自JavaSE 5.0之后, Java支持了自动装箱/拆箱,所谓自动装箱/拆箱是指:

1
2
3
int int1 = 1;
Integer integer2 = int1; //自动装箱
int int3 = integer2;  //自动拆箱

以上来自维基。要说明的是,自动装箱/拆箱中的“自动”是编译器帮忙做的,以Integer类和int类型的转化为例,编译器会在int类型转Integer包装类时调用Integer的构造函数来进行实例化,会在Integer包装类转int类型时调用intValue()方法进行拆箱。

当我开始学习Java的时候早已是Java6的时代了,没有经历过装箱/拆箱的变迁,对自动装箱/拆箱的理解停留在理论上,所以实际中踩了坑也就不足为怪了。以下是我亲身经历的两次踩坑过程:

1. if(!null)表达式。
有次我写了段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public Boolean check(){
    try{
        if(...){
            return true;
        }else{
            return false;
        }
    catch(Exception e){
    }
    return null;
}
 
public void doSth(){
    if(!check()){
        logger.error("error");
    }else{
         ...
    }
}

正常流程下check()方法返回true和false,但是偏偏我又想处理出现异常的情况,所以机智地在程序异常时返回null,试图用一个Boolean包装类来表示true,false和null三种状态。可能受C语言系语法影响吧,在外层doSth()方法的if语句中写下了!check()表达式,结果某天程序抛出一个运行时异常NullPointerException。因为编译器的自动拆箱机制,if语句处理后的代码变成if(!check().booleanValue())。如果check()方法返回null,if语句就会执行!null.booleanValue(),对一个null值调用任何方法显然都会得到一个NPE。

2. 包装类与基本类型进行关系运算。
不久前我大概写了以下代码:

1
2
3
4
5
6
JSONObject json = getHttpResponse(); // HTTP response text。
if (json.getInteger("code") != 112) {
    // error
    return;
}
...

getHttpResponse()方法调用一个HTTP restful接口,返回值是个JSONObject类型(fastjson),如果返回的json中有code属性,并且code值为112,则表示出现错误,否则,可以认为此次接口调用成功。实际上,上述程序在接口返回正确的情况下根本执行不下去,if语句中会抛出NullPointerException运行时异常。原因也是与编译器的自动拆箱有关,编译器拆箱后的if语句变为if (json.getInteger("code").intValue() != 112),显然,当HTTP接口返回值中不包含code属性时,json.getInteger("code")返回null,对null值调用intValue()会抛NPE异常。所以正确的做法是先判断json.getInteger("code")的返回值是否为null,如果不为null再去与112进行比较。

再细究下去,Java装箱/拆箱中还有一个坑,如果不知觉中写出了以下代码,那么实际上已经掉坑里了:

1
2
3
4
5
6
7
Integer i1 = getInteger1();
Integer i2 = getInteger2();
if(i1 == i2){
    System.out.println("equals");
}else{
    System.out.println("not equals");
}

其一,当==运算符的两个操作数都是对象时,它比较的是两个对象在内存中的地址。通常来说,两个对象的地址不会相同,所以要比较两个对象的值需要调用equals()方法。上述程序中编译器不会调用intValue()方法将i1和i2分别拆箱后再进行比较。其二,Java对包装类进行过一定优化,默认情况下,值为[-128,128)之间的Integer对象会缓存在内存中,也就是说,每个[-128,128)之间的Integer对象,如果它们的值相同,那么它们都是同一个对象(地址相同)。所以,上述程序中打印equals还是not equals完全视getInteger1()和getInteger2()方法的返回值而定。

--EOF--

Python中JSON操作

Python中操作JSON的模块是json,导入即可用:

1
import json

json模块主要提供两个方法:loads()和dumps(),通过这两个方法对JSON进行操作,前者用来将JSON字符串转换成Python基本类型(dict, list),后者用来将Python基本类型(dict, list, tuple)转换成JSON字符串。试几个例子就明白了:

1. json.loads()

1
2
3
4
5
6
7
8
9
10
11
#json object string to dict
>>> str = '{"url": "www.fengchangjan.com", "method": "GET"}'
>>> data_dict = json.loads(str)
>>> data_dict['url']
u'www.fengchangjan.com'
 
# json array string to list
>>> str = '[{"url": "www.fengchj.com"}, {"method": "GET"}]'
>>> data_list = json.loads(str)
>>> data_list[0]['url']
u'www.fengchj.com'

2. json.dumps()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# dict to json object string
>>> data = {'url' : 'www.fengchangjan.com', 'method' : 'GET'}
>>> json.dumps(data)
'{"url": "www.fengchangjan.com", "method": "GET"}'
 
# list to json array string
>>> data = [{'url' : 'www.fengchj.com'}, {'method' : 'GET'}]
>>> json.dumps(data)
'[{"url": "www.fengchj.com"}, {"method": "GET"}]'
 
# tuple to json array string
>>> data = ({'url' : 'www.fengchj.com'}, {'method' : 'GET'})
>>> json.dumps(data)
'[{"url": "www.fengchj.com"}, {"method": "GET"}]'

json模块还有很多其他的方法,即便是loads()和dumps()也有许多选项参数,需要时可以查看手册,此处不举例。我觉得单凭示例中的两个方法已经能满足平时JSON操作中的80%需求了。

--EOF--

ContentNegotiatingViewResolver in Spring MVC

Spring MVC框架利用ContentNegotiatingViewResolver类来进行响应视图格式的协商,客户端可以通过设置Accept Header来获取指定格式的响应体(XML或JSON),关于Accept Header的设置,这里有个小坑,今天就掉进去了。

配置文件中ContentNegotiatingViewResolver的配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<bean 
 class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver">
  <property name="order" value="1" />
  <property name="mediaTypes">
    <map>
      <entry key="xml" value="application/xml" />
      <entry key="json" value="application/json" />
    </map>
  </property>
  <property name="defaultViews">
    <list>
      <bean 
       class="org.springframework.web.servlet.view.json.MappingJacksonJsonView"/>
      <bean 
       class="org.springframework.web.servlet.view.xml.MarshallingView">
        <property name="marshaller">
          <bean class="org.springframework.oxm.xstream.XStreamMarshaller"/>
        </property>
      </bean>
    </list>
  </property>
</bean>

我用Postman Chrome插件和curl命令调了服务器端的接口GET /rest/heartbeat,返回正确。但是用Python的urllib2库发请求调用会返回500 Internal Server Error的HTTP响应,试过更底层一点的httplib库,结果也是一样,服务器端抛ServletException异常:

1
2
3
4
5
6
7
javax.servlet.ServletException: 
    Could not resolve view with name 'view' in servlet with name 'rest'
        at org.springframework.web.servlet.DispatcherServlet.render
                (DispatcherServlet.java:1162)
        at org.springframework.web.servlet.DispatcherServlet.doDispatch
                (DispatcherServlet.java:950)
        ......

后来通过监听服务器端口发现,无论Postman还是curl,如果没有显式指定Accept Header,程序会自动用*/*将它补上。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ nc -l 8080
GET /rest/heartbeat HTTP/1.1
User-Agent: curl/7.24.0 (x86_64-apple-darwin12.0) libcurl/7.24.0
Host: 127.0.0.1:8080
Accept: */*
 
$ nc -l 8080
GET /rest/heartbeat HTTP/1.1
Host: 127.0.0.1:8080
Connection: keep-alive
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2)
Accept: */*
Accept-Encoding: gzip,deflate,sdch
Accept-Language: zh-CN,zh;q=0.8

而用urllib2、httplib这些库来自定义HTTP请求,除非显式指定Accept Header,否则类库不会自动填充。所以后来在程序中加上Accept请求头就搞定了。

要分析原因,需从ContentNegotiatingViewResolver类的实现中找答案。

ContentNegotiatingViewResolver是ViewResolver接口的一个实现类,主要作用是根据请求资源类型或Accept Header来决定返回的视图形式。它本身没有渲染视图的能力,只能作为一个代理,决定选择用哪个ViewResolver进行视图渲染,如上面的配置中,defaultViews选项列出了当前可选的两类视图解析器,JSON或者XML,因为JSON排在XML之前,所以默认情况下,返回JSON格式视图。

那么,默认情况是排除了哪些情况呢?

Spring文档中有个协商算法,大致流程为:
1. 如果setFavorPathExtension(boolean)设置为true,并且客户端请求路径中带了可识别的文件扩展名(.xml, .json等),则会自动选择匹配的视图解析器处理。
2. 如果setFavorParameter(boolean)设置为true,并且客户端请求的参数中携带了可识别的用于协商视图的参数(默认为format参数),则会自动选择匹配的视图解析器处理。配置文件中的mediaTypes就是用来定义哪些扩展名(或协商参数值)是可识别的。
3. 如果上面没有匹配到,并且ignoreAcceptHeader属性设置为false,则根据客户端请求中的Accept Header来选择匹配的视图解析器处理。

假如协商算法进行到第3步,恰好客户端又没有设置Accept Header,那么服务器端就会抛ServletException异常,提示视图未找到。当客户端传来Accept: */*时,defaultViews属性就派上用场了,ContentNegotiatingViewResolver会根据defaultViews属性列表中的顺序选择一个视图解析器进行渲染。此外,如果ignoreAcceptHeader属性设置为true,那么协商算法就不考虑Accept Header了,如果1、2步没有匹配,直接抛视图未找到的ServletException异常。

另外,Spring版本不同,协商算法也略有不同,在Spring 3.1.1里表现如上所述,但在Spring 3.2.3,设置Accept为*/*不起作用了,若要返回默认视图处理器,必须指定defaultContentType属性。

--EOF--

json_lib处理null字符串bug

使用json_lib 2.2.3及以下的版本,程序中调用JSONObject.fromObject()来从字符串中转化一个json对象时,某些情况下,比如某个节点的value值是String型的“null”(含引号),经过JSONObject.fromObject()转化后,返回的json对象会把双引号去掉,变成null,也就是空值。

解决方法是升级json_lib包,升级到2.4即可解决。

如果使用maven管理依赖的话,还要添加common-collections和ezmorph的包依赖,因为json_lib 2.4的pom文件中并没有显式依赖这两个包。

1
2
3
4
5
6
7
8
9
10
11
<dependency>
         <groupId>net.sf.ezmorph</groupId>
         <artifactId>ezmorph</artifactId>
         <version>1.0.6</version>
</dependency>
 
<dependency>
         <groupId>commons-collections</groupId>
         <artifactId>commons-collections</artifactId>
         <version>3.2</version>
</dependency>

--EOF--

短网址API调用实例(PHP实现)

短网址是随着微博的流行而出现,目前市面上的短网址服务林林总总不计其数,评价一个短网址服务好坏的最重要标准是其服务的稳定性。试想,短网址服务做的工作就是为原先的长网址加一层映射关系,而这种映射关系是握在短网址服务提供商手上的,如果这个环节出了问题,导致映射关系丢失,那也就意味着再也无法找回原先的长网址。

以上差不多都是废话。

国内外比较有实力的短网址提供商如goo.gl(Google)、dwz.cn(百度)、is.gd等都提供了API的方式供外部生成短网址,该方式只需模拟一个HTTP请求,以长网址作为参数向服务器发送该请求,如果请求成功,服务器就会返回缩短以后的短网址。

有些API要求将HTTP请求以POST方式提交,例如goo.gl和dwz.cn(demo),这时可以采用基于PHP的cURL库来完成HTTP请求的发送和接收。以goo.gl为例(更多细节可参考『Google URL Shortener API』):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$apiKey = 'APIKEY'; //google短网址所需。
//API key从http://code.google.com/apis/console/得到
$postData = array('longUrl' => "http://fengchj.com", 'key' => $apiKey);
$jsonData = json_encode($postData);
$co = curl_init();
curl_setopt($co, CURLOPT_URL, 'https://www.googleapis.com/urlshortener/v1/url');
curl_setopt($co, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($co, CURLOPT_SSL_VERIFYPEER, 0);
curl_setopt($co, CURLOPT_HEADER, 0);
curl_setopt($co, CURLOPT_HTTPHEADER, array('Content-type:application/json'));
curl_setopt($co, CURLOPT_POST, 1);
curl_setopt($co, CURLOPT_POSTFIELDS, $jsonData);
$strResponse = curl_exec($co);
curl_close($co);
$arrResponse = json_decode($strResponse );
print $arrResponse->id; //得到http://goo.gl/xxxx类型的短网址。

有些短网址服务API接受以GET方式提交的长网址,例如is.gd,用户只需将长网址作为参数向提供商给出的URL发送请求即可,如:

1
print file_get_contents("http://is.gd/create.php?format=simple&url=http://fengchj.com");

实际上这里也可采用cURL库进行GET方式HTTP请求的封装,但是由于GET方式的幂等特性,用file_get_contents来请求给定URL页面更为方便。相对于cURL库,file_get_contents方式的缺点在于无法对HTTP请求过程进行错误处理。

调用外部API生成短网址的效果可参见http://fengchj.com/?page_id=1619

--EOF--