Appium 使用 java 动态加载机制模拟脚本语言的效果

bauul · June 16, 2017 · Last by bauul replied at July 29, 2017 · 2173 hits
本帖已被设为精华帖!

缘由

有时我们需要测试某个方法,需要频繁的修改这个方法,但又不想重新去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工程,提供在线执行测试工程(要处理好安全问题)

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 7 条回复 时间 点赞

不错不错

bauul #2 · June 17, 2017 作者
哄哄 回复

☺

思寒_seveniruby 将本帖设为了精华贴 19 Jun 10:50

学习了~

还有这种操作😮

表示后面看不懂

AndroidStep.txt里边是什么内容啊,文章中没给出

bauul #9 · July 29, 2017 作者
古丶月 回复

最后一段代码就是啊

bauul [已完成] 修改 ui2.0 源码,去 session 中提及了此贴 08 Nov 16:39
bauul [接口测试][TestNG] 基于 testNG 的并发测试 中提及了此贴 31 Dec 18:21
bauul [接口测试][TestNG] 基于 testNG 的并发测试 中提及了此贴 31 Dec 18:21
需要 Sign In 后方可回复, 如果你还没有账号请点击这里 Sign Up