泛型方法的返回值类型被擦除,导致录制的数据无法被正确反序列化。
在Http服务的远程调用中,HTTP请求的响应是这样定义的 @Data class ResponseResult { String errorCode; String errorMessage; Object data; } 通过这个方式,将服务端响应进行了统一。如果服务端处理请求出现问题,将通过errorCode进行告知。如果errorCode=0,则服务端处理正确,客户端可以根据约定的类型,从data中获取到服务端返回的数据。
在服务间调用时,也是这样的数据结构。为了能够使用一个统一的RemoteService方法,一般会有如下的处理。
public class RemoteService {
public <T> T doPost(String url, String body, TypeToken<T> dataType) {
Type type=dataType.getType();
MethodReturnTypeTest.ResponseResult responseResult=doPost(url,body,type);
T resultData = (T)responseResult.getData();
return resultData;
}
}
这样做的好处是,通过传入dataType,来告知RemoteService的doPost方法如何将对端传回的数据转换成调用方所需要的数据实体。
那么问题就来了。在切面中,一般通过以下方式就可以获取到方法的返回类型type
@Around("xxx") public Object doAround(ProceedingJoinPoint joinPoint) {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
Type type=method.getGenericReturnType();
//...
}
在录制回放的自动化测试场景中,如果请求匹配成功,可以将对应的依赖mock数据从文件或者某个url处获取到,然后用
gson.fromJson(str,type)
的方式实现反序列化。 如果切点是之前的doPost方法,那么恭喜你,这时候type得到的是“T”,也就是泛型。然后反序列化时,由于不知道需要转换的目标类型,直接就报错了。
由于存在泛型时,无法从切点方法的返回类型中获取到真正的返回结果,于是考虑从方法执行的结果中获取。例如切面方法中有如下的两行代码,
Object obj=joinPoint.proceed();
Class clazz=obj.getClass();
String returnType=clazz.getTypeName();
//record.setReturnType(returnType);
首先执行被切的方法,然后根据返回值来获取其Class。 由于Class类实现了Type接口,因此可以根据获得的返回值对象来获取其真正的Class,进而获取到类型。
public final class Class<T> implements Serializable,
GenericDeclaration, Type, AnnotatedElement
通过这个操作,类似<T>doPost这样的泛型方法的返回值就暴露出来了。 稍微修改一下反序列化方法,就可以实现录制文件的回放了。
String returnTypeName=record.getReturnType(); //获取类型
Class clazz=Class.forName(returnTypeName)
gson.fromJson(str,(Type)clazz)
在完成了上述操作后,笔者经过测试,可以实现绝大部分场景下的录制回放了,包括针对泛型方法的录制。但是还是在某个场景下翻车了。 通过Debug发现,当时通过方法执行后获取到的返回值类型是 "java.util.List<com.palyground.MethodReturnTypeTest$Bean>" 而在这种情况下, 直接进行Class.forName() 会抛出ClassNotFoundException,也就是找不到这个类。直接通过反射,只能生成泛型的List实例,而不能直接指定List中的元素的类型。这样就造成了另外一个非常常见的问题,
List<Bean> list = gson.fromJson(str,List.class)
gson在处理返回时,会将list当中的内容当做Object类型来处理,也就是无法实现类型的转换了。
因此,问题就变成了如何实现类似以下的效果
List<Bean> beans= new ArrayList<Bean>;
而目前所知的信息就是类似这样的信息"java.util.List<Bean>" 。 于是,想到了如下的思路 1)根据获取到的"java.util.List<Bean>" ,从中提取出"Bean"
齐活。
有如下的案例
public class MethodReturnTypeTest {
Gson gson = new Gson();
List<Bean> beans = new ArrayList<Bean>();
@BeforeEach
public void setBean(){
Bean bean = new Bean();
bean.setMethodName("name");
bean.setReturnType("list");
bean.setNumber(0);
bean.setPrice(9.98);
beans.clear();
beans.add(bean);
}
@Test
public void testFromJsonList() throws ClassNotFoundException {
String beanString="java.util.List<com.palyground.MethodReturnTypeTest$Bean>";
String dataReadFromFile=gson.toJson(beans);
String elementName=beanString.substring(beanString.indexOf("<")+1,beanString.indexOf(">"));
Class<?> clazz= Class.forName(elementName);
Class arrayClass= Array.newInstance(clazz,1).getClass();
List<Bean> beansList1= Arrays.asList(gson.fromJson(dataReadFromFile,(Type)arrayClass));
System.out.println(gson.toJson(beansList1));
assertThatJson(beans).isEqualTo(beansList1);
}
参考:
Java泛型类型擦除以及类型擦除带来的问题 - 蜗牛大师 - 博客园 (cnblogs.com)