缘由

有时我们需要测试某个方法,需要频繁的修改这个方法,但又不想重新去 run 整个程序,怎么做呢?

一个正常的 appium 测试用例

package com.dynamicclassloader;

import java.io.File;
import java.net.URL;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.remote.DesiredCapabilities;
import com.dynamicclassloader.DynamicEngine;
import com.my.Utils;

import io.appium.java_client.android.AndroidDriver;
import io.appium.java_client.remote.AndroidMobileCapabilityType;
import io.appium.java_client.remote.AutomationName;
import io.appium.java_client.remote.MobileCapabilityType;

public class CommonTest {

    private AndroidDriver<?> driver;
    public DesiredCapabilities capabilities = new DesiredCapabilities();
    public String location = "C:/Users/test.apk";
    public String pkgName = "";
    public String activityName = "";

    public String getSN() {
        String sn = Utils.runCMD(new String[]{"adb", "devices"}).split("\n")[1].split("\t")[0];

        return sn;
    }

    public void updatePkgActivity() {
        String pkgInfo = Utils.runCMD(new String[]{"aapt", "dump", "badging", location});

        String pkgStart = "package: name='";
        pkgName = pkgInfo.substring(pkgInfo.indexOf(pkgStart) + pkgStart.length(), pkgInfo.indexOf("' version"));

        String activityStart = "launchable-activity: name='";
        activityName = pkgInfo.substring(pkgInfo.indexOf(activityStart) + activityStart.length(), pkgInfo.indexOf("'  label=''"));

    }

    public void updateCapabilites() {
        File app = new File(location);

        capabilities.setCapability(MobileCapabilityType.DEVICE_NAME, getSN());

        updatePkgActivity();
        capabilities.setCapability(AndroidMobileCapabilityType.APP_PACKAGE, pkgName);
        capabilities.setCapability(AndroidMobileCapabilityType.APP_ACTIVITY, activityName);

        capabilities.setCapability(MobileCapabilityType.NEW_COMMAND_TIMEOUT, 300);

        if (Utils.runCMD(new String[]{"adb", "shell", "pm", "path", pkgName}).equals("")) {
            capabilities.setCapability(MobileCapabilityType.APP, app.getAbsolutePath());
        }

        capabilities.setCapability(MobileCapabilityType.AUTOMATION_NAME, AutomationName.ANDROID_UIAUTOMATOR2);
    }

    @Before
    public void setUp() throws Exception {
        updateCapabilites();
        driver = new AndroidDriver<WebElement>(new URL("http://127.0.0.1:4723/wd/hub"), capabilities);

    }

    @Test
    public void stepTest() {
        TouchAction ta = new TouchAction(driver);
        ta.tap(306, 1852).perform();
    }

    @After
    public void tearDown() throws Exception {
        driver.quit();
    }

}

当我修改 stepTest 后,重新运行时,就会发现 appium 日志又刷刷地从头开始创建 driver 了,虽然 driver 已经创建完成了

动态加载方法

Utils 类中访问获取程序内部资源的方法

    public String getStep(String fileName) {
        String step = "";

        InputStream is = getClass().getResourceAsStream(fileName);
        BufferedReader br = new BufferedReader(new InputStreamReader(is));
        String s = "";  
        try {
            while((s = br.readLine()) != null) {
                step += s;
//              System.out.println(s);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

        return step;
    }

修改后的 stepTest 方法

@Test
    public void stepTest() {

        while (true) {
            String source;
            DynamicEngine de = DynamicEngine.getInstance();

            source = new Utils().getStep("AndroidStep.txt");

            try {
                System.out.println("test");
                Class clazz =  de.javaCodeToObject("com.carl.AndroidStep", source);
                clazz.getMethod("runStep", AndroidDriver.class).invoke(clazz, driver);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
package com.dynamicclassloader;

import java.io.File;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.List;

import javax.tools.*;


/**
 * 在Java中最好的方法是使用StandardJavaFileManager类。
 * 这个类可以很好地控制输入、输出,并且可以通过DiagnosticListener得到诊断信息,
 * 而DiagnosticCollector类就是listener的实现。
 * 使用StandardJavaFileManager需要两步。
 * 首先建立一个DiagnosticCollector实例以及通过JavaCompiler的getStandardFileManager()方法得到一个StandardFileManager对象。
 * 最后通过CompilationTask中的call方法编译源程序。
 */
public class DynamicEngine {
    //单例
    private static DynamicEngine ourInstance = new DynamicEngine();

    public static DynamicEngine getInstance() {
        return ourInstance;
    }
    private URLClassLoader parentClassLoader;
    private String classpath;
    private DynamicEngine() {
        //获取类加载器
        this.parentClassLoader = (URLClassLoader) this.getClass().getClassLoader();

        //创建classpath
        this.buildClassPath();
    }

    /**
     * @MethodName    : 创建classpath
     */
    private void buildClassPath() {
        this.classpath = null;
        StringBuilder sb = new StringBuilder();
        for (URL url : this.parentClassLoader.getURLs()) {
            String p = url.getFile();
            sb.append(p).append(File.pathSeparator);
        }
        this.classpath = sb.toString();
        //System.out.println("classpath:" + this.classpath);
    }

    /**
     * @MethodName    : 编译java代码到Object
     * @Description    : TODO
     * @param fullClassName   类名
     * @param javaCode  类代码
     * @return Object
     * @throws Exception
     */
    public Class javaCodeToObject(String fullClassName, String javaCode) throws Exception {
        long start = System.currentTimeMillis(); //记录开始编译时间
//        Object instance = null;
        //获取系统编译器
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        // 建立DiagnosticCollector对象
        DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<JavaFileObject>();

        // 建立用于保存被编译文件名的对象
        // 每个文件被保存在一个从JavaFileObject继承的类中
        ClassFileManager fileManager = new ClassFileManager(compiler.getStandardFileManager(diagnostics, null, null));

        List<JavaFileObject> jfiles = new ArrayList<JavaFileObject>();
        jfiles.add(new StringSourceJavaObject(fullClassName, javaCode));

        //使用编译选项可以改变默认编译行为。编译选项是一个元素为String类型的Iterable集合
        List<String> options = new ArrayList<String>();
        options.add("-encoding");
        options.add("UTF-8");
        options.add("-classpath");
        options.add(this.classpath);

        JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, diagnostics, options, null, jfiles);
        // 编译源程序
        boolean success = task.call();

        Class clazz = null;
        if (success) {
            //如果编译成功,用类加载器加载该类
            JavaClassObject jco = fileManager.getJavaClassObject();
            DynamicClassLoader dynamicClassLoader = new DynamicClassLoader(this.parentClassLoader);
            //String source = "package com.carl.test;public class Sourceee { public static void main(String[] args) {System.out.println(\"Hello World!\");} }";
            clazz = dynamicClassLoader.loadClass(fullClassName, jco);
//            instance = clazz.newInstance();

            for (Method m: clazz.getDeclaredMethods()) {
                System.out.println("method name: " + m.getName());
//                Class<?> classType = Class.forName("java.lang.String");
//                m.invoke(clazz, Array.newInstance(classType, 0));
            }
        } else {
            //如果想得到具体的编译错误,可以对Diagnostics进行扫描
            String error = "";
            for (Diagnostic diagnostic : diagnostics.getDiagnostics()) {
                error += compilePrint(diagnostic);
            }
        }
        long end = System.currentTimeMillis();

        return clazz;
//        System.out.println("javaCodeToObject use:"+(end-start)+"ms");
//        return instance;
    }

    /**
     * @MethodName    : compilePrint
     * @Description    : 输出编译错误信息
     * @param diagnostic
     * @return
     */
    private String compilePrint(Diagnostic diagnostic) {
        System.out.println("Code:" + diagnostic.getCode());
        System.out.println("Kind:" + diagnostic.getKind());
        System.out.println("Position:" + diagnostic.getPosition());
        System.out.println("Start Position:" + diagnostic.getStartPosition());
        System.out.println("End Position:" + diagnostic.getEndPosition());
        System.out.println("Source:" + diagnostic.getSource());
        System.out.println("Message:" + diagnostic.getMessage(null));
        System.out.println("LineNumber:" + diagnostic.getLineNumber());
        System.out.println("ColumnNumber:" + diagnostic.getColumnNumber());
        StringBuffer res = new StringBuffer();
        res.append("Code:[" + diagnostic.getCode() + "]\n");
        res.append("Kind:[" + diagnostic.getKind() + "]\n");
        res.append("Position:[" + diagnostic.getPosition() + "]\n");
        res.append("Start Position:[" + diagnostic.getStartPosition() + "]\n");
        res.append("End Position:[" + diagnostic.getEndPosition() + "]\n");
        res.append("Source:[" + diagnostic.getSource() + "]\n");
        res.append("Message:[" + diagnostic.getMessage(null) + "]\n");
        res.append("LineNumber:[" + diagnostic.getLineNumber() + "]\n");
        res.append("ColumnNumber:[" + diagnostic.getColumnNumber() + "]\n");
        return res.toString();
    }
}


package com.dynamicclassloader;

import javax.tools.SimpleJavaFileObject;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URI;
/**
 * 将输出流交给JavaCompiler,最后JavaCompiler将编译后的class文件写入输出流中
 */
public class JavaClassObject extends SimpleJavaFileObject {

    /**
     * 定义一个输出流,用于装载JavaCompiler编译后的Class文件
     */
    protected final ByteArrayOutputStream bos = new ByteArrayOutputStream();

    /**
     * 调用父类构造器
     * @param name
     * @param kind
     */
    public JavaClassObject(String name, Kind kind) {
        super(URI.create("string:///" + name.replace('.', '/') + kind.extension), kind);
    }

    /**
     * 获取输出流为byte[]数组
     * @return
     */
    public byte[] getBytes() {
        return bos.toByteArray();
    }

    /**
     * 重写openOutputStream,将我们的输出流交给JavaCompiler,让它将编译好的Class装载进来
     * @return
     * @throws IOException
     */
    @Override
    public OutputStream openOutputStream() throws IOException {
        return bos;
    }

    /**
     * 重写finalize方法,在对象被回收时关闭输出流
     * @throws Throwable
     */
    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        bos.close();
    }
}

package com.dynamicclassloader;

import javax.tools.JavaFileObject;
import javax.tools.SimpleJavaFileObject;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;

public class StringSourceJavaObject extends SimpleJavaFileObject {
    private String content = null;

    public StringSourceJavaObject(String name, String content) throws URISyntaxException {
        super(URI.create("string:///" + name.replace(".", "/") + JavaFileObject.Kind.SOURCE.extension), Kind.SOURCE);
        this.content = content;
    }

    public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
        return content;
    }
}

package com.dynamicclassloader;

import java.net.URL;
import java.net.URLClassLoader;

public class DynamicClassLoader extends URLClassLoader {
    public DynamicClassLoader(ClassLoader parent) {
        super(new URL[0], parent);
    }

    public Class findClassByClassName(String className) throws ClassNotFoundException {
        return super.findClass(className);
    }

    public Class loadClass(String fullName, JavaClassObject jco) {
        byte[] classData = jco.getBytes();
        return super.defineClass(fullName, classData, 0, classData.length);
    }
}

package com.dynamicclassloader;


import java.io.IOException;
import javax.tools.*;

/**
 * 类文件管理器
 * 用于JavaCompiler将编译好后的class,保存到jclassObject中
 */
public class ClassFileManager extends ForwardingJavaFileManager {

    /**
     * 保存编译后Class文件的对象
     */
    private JavaClassObject jclassObject;

    /**
     * 调用父类构造器
     * @param standardManager
     */
    public ClassFileManager(StandardJavaFileManager standardManager) {
        super(standardManager);
    }

    /**
     * 将JavaFileObject对象的引用交给JavaCompiler,让它将编译好后的Class文件装载进来
     * @param location
     * @param className
     * @param kind
     * @param sibling
     * @return
     * @throws IOException
     */
    @Override
    public JavaFileObject getJavaFileForOutput(Location location, String className, JavaFileObject.Kind kind, FileObject sibling)
            throws IOException {
        if (jclassObject == null)
            jclassObject = new JavaClassObject(className, kind);
        return jclassObject;
    }

    public JavaClassObject getJavaClassObject() {
        return jclassObject;
    }
}

package com.carl;

import io.appium.java_client.TouchAction;
import io.appium.java_client.android.AndroidDriver;

public class AndroidStep {

    public static void runStep(AndroidDriver driver) {
        TouchAction ta = new TouchAction(driver);
        ta.tap(306, 1852).perform();
    }

}

主要原理

动态加载一个 java 类,并反射调用

参考书籍

《Java 深度历险》

拓展场景

允许提交 java 工程,提供在线执行测试工程(要处理好安全问题)


↙↙↙阅读原文可查看相关链接,并与作者交流