selenium PageObject源码分析

先看下入口函数

     PageFactory.initElements(new AppiumFieldDecorator(mDriver, 5, TimeUnit.SECONDS),
                mQQPageObject);

绑定注入容器(QQPageObject)

public static void initElements(FieldDecorator decorator, Object page) {
    //获取所有父类
    Class proxyIn = page.getClass();
    while (proxyIn != Object.class) {
      proxyFields(decorator, page, proxyIn);
      proxyIn = proxyIn.getSuperclass();
    }
  }

proxyFields()创建所有字段的代理类

  private static void proxyFields(FieldDecorator decorator, Object page, Class proxyIn) {
    //获取所有声明的字段
    Field[] fields = proxyIn.getDeclaredFields();
    for (Field field : fields) {
      //获取代理对象
      Object value = decorator.decorate(page.getClass().getClassLoader(), field);
      if (value != null) {
        try {
          field.setAccessible(true);
        //重新设置字段
          field.set(page, value);
        } catch (IllegalAccessException e) {
          throw new RuntimeException(e);
        }
      }
    }
  }

先获取所有字段,再获取字段的代理对象,并重新赋值给注入容器

  public Object decorate(ClassLoader ignored, Field field) {
      //使用默认的字段包装器
        Object result = defaultElementFieldDecoracor.decorate(ignored, field);
        if (result != null) {
            return result;
        }
        //Widget包装器
        return decorateWidget(field);
    }

先通过默认包装器构造代理对象,如果没有再通过widget包装器构造

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;
    }
  }

元素定位器的创建时一个多工厂模式创建的 factory.createLocator(field);
来看看factory的来源.我们在第一步PageFactory.initElements()传入了一个AppiumFieldDecorator字段包装器,

  public AppiumFieldDecorator(SearchContext context, TimeOutDuration timeOutDuration) {
      ...省略无关代码....
        defaultElementFieldDecoracor = new DefaultFieldDecorator(
          //创建了一个具体的元素定位器的工厂
            new AppiumElementLocatorFactory(context, timeOutDuration, originalDriver,
                new DefaultElementByBuilder(platform, automation))) {
          ........省略无关代码......
        widgetLocatorFactory =
            new AppiumElementLocatorFactory(context, timeOutDuration, originalDriver,
                new WidgetByBuilder(platform, automation));
    }

回到factory创建定位器的 ElementLocator locator = factory.createLocator(field);

    @Override public CacheableLocator createLocator(AnnotatedElement annotatedElement) {
         ......
        //构建一个查找元素的方法
        By by = builder.buildBy();
        if (by != null) {
        //创建一个元素定位器实现对象
            return new AppiumElementLocator(searchContext, by, builder.isLookupCached(),
                    customDuration, timeOutDuration, originalWebDriver);
        }
        return null;
    }

builder.buildBy()是如何构建一个查找元素具体方法的?其实builder是一个抽象类,我们是在AppiumFieldDecorator的构造方法中框架自动给创建一个DefaultElementByBuilder对象。buildBy()实现如下

@Override public By buildBy() {
       //断言用户使用的注解是否正确,例如AndroidFindBy 与AndroidFindBys不能同时在一个字段上使用
        assertValidAnnotations();
        //解析用户的注解并创建一个By
        //公用注解
        By defaultBy = buildDefaultBy();
        //不同系统版本的注解解析
        By mobileNativeBy = buildMobileNativeBy();
      
        String idOrName = ((Field) annotatedElementContainer.getAnnotated()).getName();

        if (defaultBy == null && mobileNativeBy == null) {
            defaultBy =
                new ByIdOrName(((Field) annotatedElementContainer.getAnnotated()).getName());
            mobileNativeBy = new By.ById(idOrName);
            return returnMappedBy(defaultBy, mobileNativeBy);
        }
      ......
    }

有了元素定位方式By后就创建AppiumElementLocator给DefaultFieldDecorator包装器使用了,回到DefaultFieldDecorator.decorate()方法

  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;
    }
  }

创建一个定位代理器

  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;
  }

这是最核心的动态代理。看看处理器LocatingElementHandler

public class LocatingElementHandler implements InvocationHandler {
  private final ElementLocator locator;

  public LocatingElementHandler(ElementLocator locator) {
    this.locator = locator;
  }

  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;
    }
    //如果调用的是getWrappedElement方法则直接返回不代理调用方法
    if ("getWrappedElement".equals(method.getName())) {
      return element;
    }

    try {
      //再调用元素的真正方法  
      return method.invoke(element, objects);
    } catch (InvocationTargetException e) {
      // Unwrap the underlying exception
      throw e.getCause();
    }
  }
}

这样也就是我们在调用WebElement.click()方法的时候回进入到代理器的invoke()方法,先查找到元素,再去真正的执行click()方法。而这个代理器,最后会设置给pageobject的字段。

 public static void initElements(FieldDecorator decorator, Object page) {
    Class proxyIn = page.getClass();
    while (proxyIn != Object.class) {
      proxyFields(decorator, page, proxyIn);
      proxyIn = proxyIn.getSuperclass();
    }
  }

  private static void proxyFields(FieldDecorator decorator, Object page, Class proxyIn) {
    Field[] fields = proxyIn.getDeclaredFields();
    for (Field field : fields) {
      Object value = decorator.decorate(page.getClass().getClassLoader(), field);
      if (value != null) {
        try {
          field.setAccessible(true);
          //把动态代理对象重新设置回来
          field.set(page, value);
        } catch (IllegalAccessException e) {
          throw new RuntimeException(e);
        }
      }
    }
  }

所以当我们在调用字段的方法时,实际是调用一个代理对象。
再看看代理器是怎么通过被代理对象locator来找元素的。locator.findElement()
从上面的分析locator是一个AppiumElementLocator

 public WebElement findElement() {
      //如果有缓存则直接用
        if (cachedElement != null && shouldCache) {
            return cachedElement;
        }

        try {
            WebElement result =  waitFor(() ->
                    //查找元素
                    searchContext.findElement(by));
            if (shouldCache) {
                cachedElement = result;
            }
            return result;
        } catch (TimeoutException | StaleElementReferenceException e) {
            throw new NoSuchElementException(exceptionMessageIfElementNotFound, e);
        }
    }

searchContext.findElement(by);其实是通过RemoteWebDriver的findElement(By by) 方法查找,又转移到By方法查找

  public WebElement findElement(SearchContext context) {
    List allElements = findElements(context);
    if (allElements == null || allElements.isEmpty())
      throw new NoSuchElementException("Cannot locate an element using "
          + toString());
    return allElements.get(0);
  }

假如这个By是ById那么转移到ById的findElement()方法中了

  @Override
    public WebElement findElement(SearchContext context) {
      if (context instanceof FindsById)
        return ((FindsById) context).findElementById(id);
      return ((FindsByXPath) context).findElementByXPath(".//*[@id = '" + id
          + "']");
    }

最后还是转移到RemoteWebDriver中

  protected WebElement findElement(String by, String using) {
    if (using == null) {
      throw new IllegalArgumentException("Cannot find elements when the selector is null.");
    }
    //这里是真正执行网络通信查找控件的方法
    Response response = execute(DriverCommand.FIND_ELEMENT,
        ImmutableMap.of("using", by, "value", using));
    //接收网络返回的WebElement
    Object value = response.getValue();
    WebElement element;
    try {
      element = (WebElement) value;
    } catch (ClassCastException ex) {
      throw new WebDriverException("Returned value cannot be converted to WebElement: " + value, ex);
    }
    //标识查找的方式
    setFoundBy(this, element, by, using);
    return element;
  }

execute方法就是拼json参数,形成httpRequest请求,通过HttpClient发送请求,
解析返回的HttpResponse,用selenium的Response接收,最终封装成我们需要的WebElement.

总结下:pageobject模式其实就是依赖注入+动态代理实现的,每一个pageobject都可以看成一个注入容器,而我们查找控件的操作看成一个切面,通过动态代理实现AOP切面编程。

selenium PageObject源码分析_第1张图片
QQ图片20180129192831.jpg

你可能感兴趣的:(selenium PageObject源码分析)