前段日子在使用google-http-client.jar 这个组件做http请求时,发现一件有趣的事情,具体代码如下:
try {
HttpTransport transport = new NetHttpTransport.Builder().doNotValidateCertificate().build();
requestFactory = transport.createRequestFactory(new HttpRequestInitializer() {
@Override
public void initialize(HttpRequest request) {
int timeout = 5 * 1000;
request.setReadTimeout(timeout);
request.setParser(new JsonObjectParser(new JacksonFactory()));
request.setThrowExceptionOnExecuteError(false);
logger.debug("set timeout = {} milliseconds", timeout);
}
});
} catch (GeneralSecurityException e) {
logger.error("init static members failed:", e);
}
HttpRequest request = requestFactory.buildPostRequest(new GenericUrl(url), content);
HttpResponse response =request.execute();
Bean ret = (Map)response.parseAs(Bean.class);
......
这是一段很简单的http请求的代码,引起我注意的是最后一段代码,并且有个疑问:
为什么HttpResponse.parseAs方法可以通过入参Bean.class就能够将结果装配到Bean类,并返回Bean类型?
事实上,HttpResponse.parseAs有两个同名的重载方法:
public T parseAs(Class dataClass) throws IOException {
if (!hasMessageBody()) {
return null;
}
return request.getParser().parseAndClose(getContent(), getContentCharset(), dataClass);
}
public Object parseAs(Type dataType) throws IOException {
if (!hasMessageBody()) {
return null;
}
return request.getParser().parseAndClose(getContent(), getContentCharset(), dataType);
}
两个入参不同,返回的类型也不同,第一个方法可以在编译期返回确切的类型,第二个只能返回Object类型,需要使用者自行强转。那么这两个方法到底有什么区别呢,既然存在肯定是为了解决什么问题吧。我们来看看这两个方法用在哪儿:
1、Bean ret = response.parseAs(Bean.class);
2、Map ret = (Map)response.parseAs(new TypeToken>() {}.getType());
相信已经有的朋友已经看出来了, 像Map,List这些带有泛型的类型是无法直接通过.class的静态变量获取的,就算我们可以通过Map.class获取到,但得到的却是Map,和Map还是不一样的。泛型存在于编译期,在运行时Map和Map的类实例(Class对象)是同一个,这是为了防止在运行期过多创建类实例,防止类型膨胀,减少运行时开销,这样的实现不可避免的就需要在运行时将泛型擦除,所以第二个parseAs方法就是为了动态的在运行时获取带泛型的实际类型,从而反序列化到该类型。泛型在运行时被擦除和在运行时获取泛型的实际类型看似矛盾的两个问题,前者表述没有问题,后者在一定条件下也是对的,为什么这么说,我们来看怎么获取运行时对象a的泛型指代的实际类型,请看如下代码:
package org.hxb.spring.generic;
import java.lang.reflect.ParameterizedType;
import java.util.Arrays;
import java.util.Map;
import org.junit.Test;
public class GenericTest {
@Test
public void test1() {
Bean> a = new Bean>();
System.out.println(a.getClass().getGenericSuperclass().getTypeName());
ParameterizedType type = (ParameterizedType) a.getClass().getGenericSuperclass();
if (type.getActualTypeArguments() != null) {
System.out.println(Arrays.asList(type.getActualTypeArguments()));
}
}
@Test
public void test2() {
Bean> a = new Bean>() {
};
ParameterizedType type = (ParameterizedType) a.getClass().getGenericSuperclass();
if (type.getActualTypeArguments() != null) {
System.out.println(Arrays.asList(type.getActualTypeArguments()));
}
}
}
class Father {
}
class Bean extends Father {
}
输出:
[T]
[java.util.Map]
有人会问我,为什么Bean要继承一个Father? 因为不这么做会导致(ParameterizedType)a.getClass().getGenericSuperclass()语句报cast exception,getGenericSuperclass方法jdk 1.5 之后加入的,返回直接父类,继承的父类。(泛型也是同期引入的,同期引入的还有接口java.lang.reflect.Type,以及一些和java.lang.Class 同级别的实现类如ParameterizedType等),那第二Test为什么可以得到运行时真实类型?不知道大家也没有注意到这个细微的差别:
Bean> a = new Bean>();
Bean> a = new Bean>(){};
下面那句话多了一对花括号,相信大家都知道这是什么意思,这样就创建了一个匿名类,
第一种方法显示a的类型是Bean
第一种方法显示a的类型是GenericTest$1
匿名类继承类型Bean>,而这个匿名类是在运行时定义的,所以保留了泛型的实际类型(实际就是相当于Bean extends Father,此时继承的是确定类型)
所以getGenericSuperclass方法返回一个ParameterizedType的结果,然后通过ParameterizedType的getActualTypeArguments方法便可以获取实际的类型,实际上用这种方法的话Bean就无需在编译器继承某个父类了,直接在运行时声明一个匿名类即可:
package org.hxb.spring.generic;
import java.lang.reflect.ParameterizedType;
import java.util.Arrays;
import java.util.Map;
import org.junit.Test;
public class GenericTest {
@Test
public void test2() {
Bean> a = new Bean>() {
};
ParameterizedType type = (ParameterizedType) a.getClass().getGenericSuperclass();
if (type.getActualTypeArguments() != null) {
System.out.println(Arrays.asList(type.getActualTypeArguments()));
}
}
}
class Bean {
}
上述代码亦可以输出实际类型。
回到HttpResponse的第二parseAs方法的用法:Map ret = (Map)response.parseAs(new TypeToken>() {}.getType()),通过上面的分析,我们可以知道,TypeToken.getType()方法其实也是用来获取泛型的实际类型的,这样就可以将响应反序列化为带泛型的类型了。我们可以做如下实验:
package org.hxb.spring.generic;
import java.lang.reflect.ParameterizedType;
import java.util.Map;
import org.junit.Test;
import com.google.common.reflect.TypeToken;
public class GenericTest {
@Test
public void test2() {
Bean> a = new Bean>() {
};
ParameterizedType type = (ParameterizedType) a.getClass().getGenericSuperclass();
if (type.getActualTypeArguments() != null) {
System.out.println(type.getActualTypeArguments()[0]);
}
}
@Test
public void test3() {
System.out.println(new TypeToken>() {}.getType());
}
}
class Bean {
}
实际输出:
实验结果和我们猜想的那样,我们再看看TypeToken的无参构造方法,
无参构造方法的访问权限是protected,有人会问了,那我怎么实例化?呵呵,其实作者的意图就是为了确保你不能直接实例化TypeToken,但是我们可以用匿名实现类直接继承TypeToken并实例化(就是多了对花括号{})。
无参构造方法调用了父类的capture(捕获)方法,从截图中可以看到,该方法调用了getGenericSuperclass,返回并且判断父类的类型是不是ParameterizedType,不是的话便抛出异常,是就返回第一个。这也验证了我们的想法,其实parseAs方法就是用了上面的原理。
在很多反序列化的开源组件中,都用了这个原理例如com.fasterxml.jackson.databind.ObjectMapper.ObjectMapper 的readValue方法,所以我们会经常见到实例化的时候会多个花括号。