基於Dubbo的http自動測試工具分享

公司是採用微服務來作模塊化的,各個模塊之間採用dubbo通訊。好處就不用提了,省略了以前模塊間複雜的http訪問。不過也遇到一些問題:前端

PS: Github的代碼示例java

測試須要配合寫消費者的代碼

對於開發來講,卻是挺省勁。可是對於測試來講就有點麻煩了, 每次還要去寫dubbo的消費程序,並且每次新增一個接口,都須要從新改寫程序,費時費力。jquery

接口返回的結果沒法定製

因爲我這邊是作一些商品的推薦,每次結果的類型都是相同的,只是內部的算法不一樣。不過接口只是返回id,沒法直觀的判斷商品類似程度或者用戶的偏好程度,須要一個可視化的返回結果界面。git

因而在這種需求下,我設想了一個小程序,它能夠知足下面的功能:github

  1. 測試能夠根據測試須要,在界面自動選擇請求的class和方法
  2. 開發完成後,測試界面自動掃描出dubbo的提供者的class和對應的方法
  3. 返回結果自動請求對應的圖片和文字說明

提早放一個效果圖:
web

1 掃描某個包下全部的類

小程序開始的第一步就是須要掃描某個包下全部的dubbo實現類。ajax

因爲工程是springboot,所以最終部署是在jar中。這時,就須要面臨兩個問題,若是是在開發工具中,如何獲取包下的全部類;若是是在jar中,如何獲取包下全部的類。算法

首先經過classloader能夠加載特定路徑下的全部URL:spring

Enumeration<URL> dirs = Thread.currentThread().getContextClassLoader().getResources(packageDirName);
while (dirs.hasMoreElements()) {
    URL url = dirs.nextElement();
    //若是是jar,則採用JarURLConnection的方式鏈接,得到class文件
    if("jar".equals(url.getProtocol())){
        findClassesInJar(url,classes,pack);
    }else{
        findClassesInSrc(url,classes,pack);
    }
}

在工程中

在工程中,class實際上是以目錄形式存放在本地的,直接按照file的方式遍歷掃描class文件就好了:小程序

public static void findClassesInSrc(URL url, Set<Class<?>> classes, String basePackage) throws UnsupportedEncodingException {

    File dir = new File(URLDecoder.decode(url.getFile(), "UTF-8"));

    if (!dir.exists() || !dir.isDirectory()) {
        return;
    }

    Arrays.stream(dir.listFiles())
            .forEach(file -> {
                String className = file.getName().substring(0, file.getName().length() - 6);
                try {
                    classes.add(Thread.currentThread().getContextClassLoader().loadClass(basePackage + '.' + className));
                } catch (ClassNotFoundException e) {
                    e.printStackTrace();
                }
            });
}

在jar包中

jar包是一種特殊的壓縮包,java提供了JarFile類的entries方法,能夠遍歷jar中全部的文件。不過這裏就沒有目錄或者文件的區別了,由於都是一個zip包中的資源而已。所以最後須要針對報名進行一下過濾:

public static void findClassesInJar(URL url,Set<Class<?>> classes,String basePackage) throws IOException, ClassNotFoundException {
    //轉換爲JarURLConnection
    JarURLConnection connection = (JarURLConnection) url.openConnection();
    if (connection != null) {
        JarFile jarFile = connection.getJarFile();
        if (jarFile != null) {
            //獲得該jar文件下面的類實體
            Enumeration<JarEntry> jarEntryEnumeration = jarFile.entries();
            while (jarEntryEnumeration.hasMoreElements()) {
                JarEntry entry = jarEntryEnumeration.nextElement();
                String jarEntryName = entry.getName();
                //這裏咱們須要過濾不是class文件和不在basePack包名下的類
                if (jarEntryName.contains(".class") && jarEntryName.replaceAll("/",".").startsWith(basePackage)) {

                    String className = jarEntryName
                            .substring(0, jarEntryName.lastIndexOf("."))
                            .replace("/", ".");

                    classes.add(Thread.currentThread().getContextClassLoader().loadClass(className));
                }
            }
        }
    }
}

2 掃描某個class下全部的方法

得到某個類的全部方法

而後經過反射能夠直接經過class的名字,拿到它的全部方法,這些方法裏面包含了一些通用的方法,如wait,notify等,須要給過濾掉。

public static Set<String> NORMAL_METHODS = new HashSet<>(Arrays.asList("wait","equals","toString","hashCode","getClass","notify","notifyAll"));
public static List<Method> getMethod(String className){
    try {

        Class clazz = Thread.currentThread().getContextClassLoader().loadClass(className);
        Method[] methods = clazz.getMethods();

        return Arrays.stream(methods)
                .filter(method -> !NORMAL_METHODS.contains(method.getName()))
                .collect(Collectors.toList());

    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }
    return new ArrayList<>();
}

這裏須要注意,兩個不一樣參數的方法,雖然名字相同,可是他們的parameterTypes是不一樣的。所以這裏最好直接返回method,把name和parameterTypes一同做爲結果返回。由於最終invoke的時候,還得經過參數類型把全部的參數都轉換類型一下。

3 方法的執行

第三個難點,就是前端傳過來的參數都是字符串,好比:

  • com.xingoo.test.Provider1Impl 是對應的class
  • test1 是對應的方法
  • 100 是對應的參數
  • java.lang.Long 是參數對應的類型

怎麼能把請求經過正確的dubbo provider執行呢?——答案 就是Bean

由於在Spring的項目中,dubbo的provider都是一個單例的bean。所以能夠直接經過applicationContext得到對應的bean,只要保證bean的名字能規律的映射過來就行。

能夠參考下面的獲取bean的方法:

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

@Component
public class SpringUtils implements ApplicationContextAware {

    private static ApplicationContext applicationContext = null;
// 非@import顯式注入,@Component是必須的,且該類必須與main同包或子包
    // 若非同包或子包,則需手動import 注入,有沒有@Component都同樣
    // 可複製到Test同包測試

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        if(SpringUtils.applicationContext == null){
            SpringUtils.applicationContext  = applicationContext;
        }
    }

    //獲取applicationContext
    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }

    //經過name獲取 Bean.
    public static Object getBean(String name){
        return getApplicationContext().getBean(name);

    }

    //經過class獲取Bean.
    public static <T> T getBean(Class<T> clazz){
        return getApplicationContext().getBean(clazz);
    }

    //經過name,以及Clazz返回指定的Bean
    public static <T> T getBean(String name,Class<T> clazz){
        return getApplicationContext().getBean(name, clazz);
    }

}

在真正的實現類上,須要指定bean的名字:

@Service("Provider1Impl")
public class Provider1Impl implements ProviderApi {
...
}

而後利用反射,就能夠執行這個bean的特定方法了:

// 反射拿到對應的class
Class cla = Thread.currentThread().getContextClassLoader().loadClass(clazz);

// 在appContext中拿到對應的bean
Object bean = SpringUtils.getBean(cla.getSimpleName());

// 格式化參數與參數類型
Class<?>[] parameterTypes = DubboApiUtils.paramTypeFormat(types);
Object[] parameters = DubboApiUtils.paramsFormat(params,types);

// 經過反射調用對應的方法
return cla.getMethod(method, parameterTypes).invoke(bean,parameters);

對應參數處理的兩個方法是:

/**
     * 根據字符串拼接,得到對應的參數類型數組
     * @param types
     * @return
     */
    public static Class<?>[] paramTypeFormat(String types){
        List<Class<?>> paramsClasses = new ArrayList<>();
        for(String type : types.split(",")){
            try {
                paramsClasses.add(Class.forName(type));
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
        return paramsClasses.toArray(new Class[]{});
    }

    /**
     * 根據參數類型,轉換類型
     * @param paramStr
     * @param types
     * @return
     */
    public static Object[] paramsFormat(String paramStr,String types){
        Class<?>[] classes = paramTypeFormat(types);
        List<Object> formats = new ArrayList<>();
        String[] params = paramStr.split(",");
        for(int i =0;i<classes.length; i++){
            //todo 簡單粗暴,有其餘的須要再加吧
            if("Long".equals(classes[i].getSimpleName())){
                formats.add(Long.valueOf(params[i]));
            }else{
                formats.add(params[i]);
            }
        }
        return formats.toArray();
    }

4 商品自動請求描述信息

最後就是jquery基於ajax請求,查詢對應的接口結果就好了。須要注意ajax的同步問題:

$.ajax({
    type : "post",
    url : "xxxxx",
    data : {xxx:xxx},
    async : false,
    success : function(r){
        
            //todo
        
    }
});

總結

總結來講,下面是遇到的問題和簡單的對應辦法:

  • 1 如何掃描工程或者普通web項目 某個包下的class——經過classloader得到路徑,直接遍歷file便可
  • 2 如何掃描jar中某個包下的class——經過JarFile得到對應的JarEntry
  • 3 如何獲取Spring Boot中的Bean——經過實現ApplicationContextAware接口,獲取applicationContext的引用
  • 4 如何動態執行某個對象的特定方法——基於反射method.invoke,須要注意傳入的參數與類型問題

經過這樣一個小工具,又對反射有了更進一步的瞭解。:-)))))))))

參考:

相關文章
相關標籤/搜索