服务接口扫描
一、概述
前段时间在测试环境部署了jvm-sandbox-repeater,成功录制到请求记录。鉴于项目中出现过业务漏测的情况(有服务的新接口未覆盖到),所以想实现一个接口覆盖的功能。
主要原理是通过对比录制的接口记录和扫描到的服务接口,就可以知道在测试时间段内,哪些接口没有被覆盖到。
二、编写Module
2.1 获取SpringClassLoad和controller类的路径
这里当sandbox被加载完成时,新建了两个EventWatchBuilder。
一个针对refresh方法,获取到spring的加载类
另一个针对buildDefaultBeanName方法,获取到带“controller”的类路径名称集合
代码如下:
@Override
public void loadCompleted() {
new EventWatchBuilder(moduleEventWatcher)
.onClass("org.springframework.context.support.AbstractApplicationContext")
.onBehavior("refresh")
.onWatching()
.withCall()
.onWatch(new AdviceListener() {
@Override
protected void before(Advice advice) throws Throwable {
if (springClassLoadIsNull()) {
setSpringClassLoad(advice.getBehavior().getDeclaringClass().getClassLoader());
}
}
});
new EventWatchBuilder(moduleEventWatcher)
.onClass("org.springframework.context.annotation.AnnotationBeanNameGenerator")
.onBehavior("buildDefaultBeanName")
.onWatching()
.withCall()
.onWatch(new AdviceListener() {
@Override
protected void before(Advice advice) throws Throwable {
Object o=advice.getParameterArray()[0];//BeanDefinition
IBeanDefinition beanDefinition = InterfaceProxyUtils.puppet(IBeanDefinition.class, o);
String s = beanDefinition.getBeanClassName();
if(StringUtils.containsIgnoreCase(s,"controller")){
if(!controllerPackageNames.contains(s)){
lifeCLogger.debug("===="+s);
controllerPackageNames.add(s);
}
}
}
});
}
2.2 获取类路径名称集合
这里编写了一个Command,给调用服务返回类路径名称集合
@Command("controllerPackageNames")
public void controllers(final PrintWriter writer){
lifeCLogger.debug("controllerPackageNames");
writer.println(JSONObject.toJSONString(controllerPackageNames));
writer.flush();
}
2.3 组建接口信息
有了加载类和类路径后,就可以通过反射,在controller类对象中拿到所有注解信息,再做一些处理,就可以完整的接口信息了。
2.3.1 编写一个Command
这里编写了一个处理单个controller类的Command,具体遍历controller类路径名称集合的逻辑在另外的调用服务中实现即可。
@Command("uris")
public void uris(final Map<String, String> param, final PrintWriter writer) {
final String appName = getParameter(param, "appName");
final String env = getParameter(param, "env");
final String packageName = getParameter(param, "packageName");
final String version = getParameter(param, "version");
if (springClassLoadIsNull()) {
lifeCLogger.debug("还没有获取到SpringClassLoad");
writer.println("error: SpringClassLoad is null");
} else {
if (null == packageName && null == appName && null == env) {
lifeCLogger.debug("appName,env,packageName 有一个参数为空");
writer.println("error: one of params(appName env packageName) is null");
} else {
lifeCLogger.debug("当前spring类加载器 SpringClassLoad :{}", this.springClassLoad.getClass().getName());
lifeCLogger.debug("generate uris for {} - {} begin", env, appName);
try {
Class<?> targetClass = this.springClassLoad.loadClass(packageName);
//获取basePath
String basePath = takeBasePath(targetClass);
//遍历method获取最终的结果
List<Map<String, String>> resultList = traversalMethods(targetClass, basePath, appName, env, packageName,version);
writer.println(JSONObject.toJSONString(resultList));
lifeCLogger.debug("generate uris for {} - {} result: {} ", env, appName, JSONObject.toJSONString(resultList));
lifeCLogger.debug("generate uris for {} - {} end ", env, appName);
} catch (ClassNotFoundException e) {
e.printStackTrace();
lifeCLogger.debug(e.getMessage());
} catch (Exception e) {
e.printStackTrace();
lifeCLogger.debug(e.getMessage());
} finally {
writer.flush();
}
}
}
}
2.3.2 获取Base路径的实现
public String takeBasePath(Class<?> targetClass) {
Annotation[] annotations=targetClass.getAnnotations();
for(Annotation annotation:annotations){
if(annotation.annotationType().getSimpleName().equals("RequestMapping")){
String[] s=(String[]) InstanceUtils.doMethodByName("value",annotation);
lifeCLogger.debug(JSONObject.toJSONString(s));
String basePath=s[0];
//处理一下basePath 保证格式是/123/456
if (!basePath.startsWith("/")) {
basePath = "/" + basePath;
}
if (basePath.endsWith("/")) {
basePath = basePath.substring(0, basePath.length() - 1);
}
if("/".equals(basePath)){
basePath="";
}
return basePath;
}
}
return "";
}
2.3.3 获取子路径实现
public Map<String, String> handleSubPath(Method method, String basePath, String appName, String env, String packageName,String version) {
Map<String, String> resultMap = new HashMap<>();
Annotation[] annotations = method.getAnnotations();
for (Annotation annotation : annotations) {
if (annotation.annotationType().getSimpleName().equals("RequestMapping")) {
resultMap.put("appName", appName);
resultMap.put("env", env);
resultMap.put("packageName", packageName);
resultMap.put("version",version);
String[] values = (String[]) InstanceUtils.doMethodByName("value", annotation);
String path = values[0];
if(null!=path){
if(!path.startsWith("/")){
path="/"+path;
}
if(path.endsWith("/")){
path = path.substring(0, path.length() - 1);
}
if("/".equals(path)){
resultMap.put("uri", basePath);
}else{
resultMap.put("uri", basePath + path);
}
}else{
resultMap.put("uri", basePath);
}
Object requestMethods_object = InstanceUtils.doMethodByName("method", annotation);
String re = JSONObject.toJSONString(requestMethods_object);
List<RequestMethod> requestMethods = JSONObject.parseArray(re, RequestMethod.class);
StringBuffer methodString = new StringBuffer();
if (requestMethods.size() > 0) {//处理method格式
for (int i = 0; i < requestMethods.size(); i++) {
methodString.append(requestMethods.get(i));
if (i != requestMethods.size() - 1) {
methodString.append(",");
}
}
}
resultMap.put("method", methodString.length() > 0 ? methodString.toString() : "default");
}
if (annotation.annotationType().getSimpleName().equals("PostMapping")) {
resultMap.put("appName", appName);
resultMap.put("env", env);
resultMap.put("packageName", packageName);
resultMap.put("version",version);
resultMap.put("method", "post");
String[] values = (String[]) InstanceUtils.doMethodByName("value", annotation);
String path = values[0];
if (null != path && !path.startsWith("/")) {
resultMap.put("uri", basePath + "/" + path);
} else {
resultMap.put("uri", basePath + path);
}
}
if (annotation.annotationType().getSimpleName().equals("GetMapping")) {
resultMap.put("appName", appName);
resultMap.put("env", env);
resultMap.put("packageName", packageName);
resultMap.put("version",version);
resultMap.put("method", "get");
String[] values = (String[]) InstanceUtils.doMethodByName("value", annotation);
String path = values[0];
if (null != path && !path.startsWith("/")) {
resultMap.put("uri", basePath + "/" + path);
} else {
resultMap.put("uri", basePath + path);
}
}
}
return resultMap;
}
三、结果
3.1接口信息
3.2 接口覆盖情况
这里利用钉钉群消息功能,将汇总消息发送通知到项目群中
四、总结
1. 接口覆盖不如代码覆盖精确,但是也可以避免主功能漏测。
2. 通过测试环境中录制的记录,也可以分析出测试了哪些场景。
3. 上面只给出了Module的编写方法,关于调用服务的代码比较简单,这里没有给出。
五、参考
Spring组件注册注解之@ComponentScan,@ComponentScans