Java做UI自动化和app自动化中动态代理@FindBy的工作原理一、背景简介
由于Selenium框架采用PageObject设计模式让测试代码与被测页面对象代码分离,因而提供了不少很方便的注解来达到目的,其中有一个注解就是@FindBy。在使用中,只要通过在field中使用注解,则可以将不同属性的元素对象转换成一个WebElement对象。
通过WebElement提供的方法,则可以进行UI层面的操作了,下面来简单看看这个神奇的注解是怎么工作的。二、注解@FindBy的使用
@FindBy(name = "修改密码")
public WebElement changePswTab;
通过指定name属性,可以将changePswTab转换成当前页面的一个WebElement对象
代码示例如下:
三、注解定义
注解的定义很简单,直接看源码即可,通过注解定义可以知道,可以通过id,name,className,xpath等多种方式来锁定当前元素
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.TYPE})
public @interface FindBy {
How how() default How.UNSET;
String using() default "";
String id() default "";
String name() default "";
String className() default "";
String css() default "";
String tagName() default "";
String linkText() default "";
String partialLinkText() default "";
String xpath() default "";
}
四、注解行为注入
注解的行为肯定是在使用前注入的,那这个@FindBy具体做了什么呢?
1.将元素构建成By对象
public By buildBy() {
assertValidAnnotations();
By ans = null;
...
FindBy findBy = field.getAnnotation(FindBy.class);
if (ans == null && findBy != null) {
ans = buildByFromFindBy(findBy);
}
...
return ans;
}
在Annotations中,selenium通过反射拿到findby对象,然后将对象构建成By对象
protected By buildByFromShortFindBy(FindBy findBy) {
if (!"".equals(findBy.className()))
return By.className(findBy.className());
...
if (!"".equals(findBy.name()))
return By.name(findBy.name());
...
// Fall through
return null;
}
public static By name(final String name) {
if (name == null)
throw new IllegalArgumentException(
"Cannot find elements when name text is null.");
return new ByName(name);
}
表面上,注解执行到这里就完成了,但是其实可以发现,在field注解使用时,拿到的是一个webelement对象!并不是现在拿到的by对象?这是怎么一回事呢?其实在这里,selenium使用了动态代理的方式来讲by对象转成webelement对象!五、动态代理转换实现
通过回溯findBy的调用方法,可以回溯到PageObject.initElements(...),但其实这个调用中,起作用的是这一段
public Object decorate(ClassLoader loader, Field field) {
if (!(WebElement.class.isAssignableFrom(field.getType())
|| isDecoratableList(field))) {
return null;
}
ElementLocator locator = factory.createLocator(field);
if (locator == null) {
return null;
}
if (WebElement.class.isAssignableFrom(field.getType())) {
return proxyForLocator(loader, locator);
} else if (List.class.isAssignableFrom(field.getType())) {
return proxyForListLocator(loader, locator);
} else {
return null;
}
}
selenium通过factory.createLocator(field)来实现by对象的构建,然后将by转换成
webelement则是proxyForLocator(loader, locator)来实现的。
protected WebElement proxyForLocator(ClassLoader loader, ElementLocator locator) {
InvocationHandler handler = new LocatingElementHandler(locator);
WebElement proxy;
proxy = (WebElement) Proxy.newProxyInstance(
loader, new Class[]{WebElement.class, WrapsElement.class, Locatable.class}, handler);
return proxy;
}
动态代理中,真正调用的是InvocationHandler的实现对象. 当调用代理对象的接口时, 实际上会通过InvocationHandler.invkoe将调用转发给实际的对象,即new LocatingElementHandler(locator),所以只需要看看LocatingElementHandler的invoke方法做了啥就知道了。
public Object invoke(Object object, Method method, Object[] objects) throws Throwable {
WebElement element;
try {
element = locator.findElement();
} catch (NoSuchElementException e) {
if ("toString".equals(method.getName())) {
return "Proxy element for: " + locator.toString();
}
throw e;
}
if ("getWrappedElement".equals(method.getName())) {
return element;
}
try {
return method.invoke(element, objects);
} catch (InvocationTargetException e) {
// Unwrap the underlying exception
throw e.getCause();
}
}
很明显,调用了findElement()方法,这样就实现了转换。六、为什么要用动态代理
因为在对一个页面进行测试时,涉及到很多元素,但真正执行可能只用到其中几个元素而已,使用动态代理的好处就是,不用在我们设计实现的时候就指定某一个代理类来代理哪一个被代理对象, 而把这种指定延迟到程序运行时由JVM来实现,相当于用例执行,元素调用时才去加载元素,简单来说就是实现了元素延迟加载。
七、关于动态代理的解释==》JDK获取动态代理对象
jdk获取动态代理类对象的步骤:
1、创建原始对象----之后的功能方法以及类加载器会用到
UserService service=new UserServiceImpl();
2、 jdk创建动态代理对象
Proxy.newProxyInstance(classLoder,interfaces,invocationHandler);
1)首先我们必须知道:Java的java.lang.reflect包下提供了一个Proxy类和一个InvocationHandler接口,还有一个Method类,通过使用类和接口可以生成JDK动态代理类或动态代理对象。 也就是:
1、Proxy提供用于创建动态代理类和代理对象的静态方法,也是所有动态代理类的父类。
2、invocationHandler(接口):表示你的代理类要干什么,额外功能的编写,接口中就一个方法 invke():表示代理对象要执行的功能代码,我们的功能(额外功能+原始类功能)就写在这个方法中
方法原型:public Object invoke(Object proxy, Method method, Object[] args){}
参数:
--object proxy:jdk创建的代理对象,无需复制
--Method method:目标类中的方法
--Object[] args:目标类中方法的参数
3、Method
作用:通过method.invoke(目标对象,方法的参数) 可以执行某个目标类方法
2)proxy提供了这样的一个静态方法来创建动态代理类对象
static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandlerh)
所以我们只需要使用这个方法就能获取jdk动态代理的对象
1、ClassLoader loader : 类加载器
作用:通过类加载器把对应类(目标类)的字节码文件加载到JVM中
获取:每一个类的class文件,自动分配与之对应的类加载器
但是:动态代理类没有对应的字节码文件,JVM也不会为它分配类加载器,所以我们需要借一个-----classLoder=UserService.class.getClassLoader();
2、Class<?>[] interfaces:接口,目标对象实现的接口--也是通过反射获得的
interfaces=service.getClass().getInterfaces();
3、InvocationHandlerh:代理类需要完成的功能--这里通过匿名内部类实现
@参数详解请上翻
InvocationHandler invocationHandler = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("前置增强的代码执行-----额外功能");
Object returnValue = method.invoke(service, args); // 原始方法的执行
return returnValue;
}
};
我们现在已经实现了获取动态代理类对象方法的实参值
编码步骤:
1、创建原始类对象
2、获取类加载器
3、获取原始类实现的接口
4、额外功能
5、构建动态代理对象并赋予实参
UserService userService = (UserService)Proxy.newProxyInstance(classLoader, interfaces, invocationHandler); // 代理对象
八、关于@FindBy的工作原理、JDK获取动态代理对象、CGLIB获取动态代理对象和AOP参考了如下文章:
http://t.zoukankan.com/liujiarui-p-12408742.html
https://blog.csdn.net/qq_44774198/article/details/12601997
https://blog.csdn.net/m0_62884713/article/details/127085803
https://blog.csdn.net/akisi/article/details/126339951
https://www.jianshu.com/p/cb8c3258839a
https://zhuanlan.zhihu.com/p/39545755/