kagamihogeの日記

kagamihogeの日記です。

Selenium Gridをdockerで使う

Selenium Gridをdockerコンテナでリモートブラウザを起動するやり方で使用する。

ソースコードと手順

docker-compose.yml

DockerでSelenium Gridを構築してクロスブラウザテストを自動化するを参考にdocker-compose.ymlを作成する。

version: '3.8'

services:
  selenium-hub:
    image: selenium/hub:4.7.2
    ports:
      - 4444:4444
      - 4442:4442
      - 4443:4443

  node-chrome:
    image: selenium/node-chrome:4.7.2
    depends_on:
      - selenium-hub
    environment:
      - TZ=Asia/Tokyo
      - HUB_HOST=selenium-hub
      - SE_EVENT_BUS_HOST=selenium-hub
      - SE_EVENT_BUS_PUBLISH_PORT=4442
      - SE_EVENT_BUS_SUBSCRIBE_PORT=4443

  node-firefox:
    image: selenium/node-firefox:4.7.2
    depends_on:
      - selenium-hub
    environment:
      - TZ=Asia/Tokyo
      - HUB_HOST=selenium-hub
      - SE_EVENT_BUS_HOST=selenium-hub
      - SE_EVENT_BUS_PUBLISH_PORT=4442
      - SE_EVENT_BUS_SUBSCRIBE_PORT=4443
docker-compose up

で起動して http://localhost:4444/ui で下記キャプチャのようなコンソールが開けばOK.

java

javaseleniumのコードを書く。

plugins {
    id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.seleniumhq.selenium:selenium-java:4.7.2'
}

https://transit.yahoo.co.jp/ にアクセスしてtitle出力するだけのサンプルコード。

import java.net.MalformedURLException;
import java.net.URL;
import java.time.Duration;

import org.openqa.selenium.By;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.remote.RemoteWebDriver;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;

public class RemoteSeleniumSample {

  public static void main(String[] args) throws MalformedURLException {
    URL address = new URL("http://localhost:4444");
    ChromeOptions chromeOption = new ChromeOptions();
    chromeOption.setHeadless(true);
    chromeOption.addArguments("--disable-gpu", "--disable-dev-shm-usage");

    RemoteWebDriver driver = new RemoteWebDriver(address, chromeOption);
    try {
      driver.get("https://transit.yahoo.co.jp/");
      WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
      wait.until(ExpectedConditions.presenceOfAllElementsLocatedBy(By.name("from")));
      System.out.println(driver.getTitle());
    } finally {
      driver.quit();
    }
  }
}

複数コンテナ起動

設定で複数ブラウザセッションを起動出来るし、複数コンテナ起動して並列実行も出来る。

 docker-compose up --scale node-chrome=3

javaから3スレッドでseleinumを起動する。

import java.net.MalformedURLException;
import java.net.URL;
import java.time.Duration;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

import org.openqa.selenium.By;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.remote.RemoteWebDriver;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;

public class RemoteSeleniumSample2 {

  public static void main(String[] args) throws MalformedURLException, InterruptedException, ExecutionException {
    ExecutorService es = Executors.newFixedThreadPool(3);
    Future<?> f1 = es.submit(() -> execute());
    Future<?> f2 = es.submit(() -> execute());
    Future<?> f3 = es.submit(() -> execute());

    f1.get();
    f2.get();
    f3.get();

    es.shutdown();
  }

  static void execute() {
    RemoteWebDriver driver = null;
    try {
      URL address = new URL("http://localhost:4444");

      ChromeOptions chromeOption = new ChromeOptions();
      chromeOption.setHeadless(true);
      chromeOption.addArguments("--disable-gpu", "--disable-dev-shm-usage");

      driver = new RemoteWebDriver(address, chromeOption);
      WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));

      driver.get("https://transit.yahoo.co.jp/");
      wait.until(ExpectedConditions.presenceOfAllElementsLocatedBy(By.name("from")));

      System.out.println(driver.getTitle());
    } catch (MalformedURLException e) {
      e.printStackTrace();
    } finally {
      driver.quit();
    }
  }

}

コンソール画面は下記のように3個のchromeブラウザ用インスタンスが作成される。また、実行中かどうかも分かる。

ハマった点

unknown error: session deleted because of page crash from tab crashed

メモリ省力化設定を入れて「Selenium::WebDriver::Error::UnknownError: unknown error: session deleted because of page crash」が出ないようにする に従って各種オプションを設定する。

Apache Commons Poolのサンプル

object poolを自力で実装する機会は余りないが、Apache Commons Poolが便利な局面があったのでその使い方をメモしておく。

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.0.0'
    id 'io.spring.dependency-management' version '1.1.0'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    developmentOnly 'org.springframework.boot:spring-boot-devtools'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    implementation 'org.apache.commons:commons-pool2'
    implementation 'org.apache.commons:commons-lang3'
}

tasks.named('test') {
    useJUnitPlatform()
}

spring-bootと直接の関係は無いけど現実的には一緒に使うだろうし、versionも省略出来るので、こうしている。commons-lang3も無関係だけど、説明用のランダム文字列生成のために使う。

import java.util.NoSuchElementException;

import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.pool2.BasePooledObjectFactory;
import org.apache.commons.pool2.ObjectPool;
import org.apache.commons.pool2.PooledObject;
import org.apache.commons.pool2.PooledObjectFactory;
import org.apache.commons.pool2.impl.DefaultPooledObject;
import org.apache.commons.pool2.impl.GenericObjectPool;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;

public class CommonsPool2Sample {

  public static void main(String[] args) throws NoSuchElementException, IllegalStateException, Exception {
    PooledObjectFactory<String> factory = new BasePooledObjectFactory<String>() {

      @Override
      public String create() throws Exception {
        String created = RandomStringUtils.randomAlphanumeric(5);
        System.out.println("created:" + created);
        return created;
      }

      @Override
      public PooledObject<String> wrap(String obj) {
        return new DefaultPooledObject<>(obj);
      }
    };

    GenericObjectPoolConfig<String> config = new GenericObjectPoolConfig<>();
    config.setMaxTotal(3);

    try (ObjectPool<String> pool = new GenericObjectPool<>(factory, config)) {
      pool.addObjects(config.getMaxTotal());

      String a1 = pool.borrowObject();
      String a2 = pool.borrowObject();
      String a3 = pool.borrowObject();
      System.out.println(a1 + " " + a2 + " " + a3);

      pool.returnObject(a1);

      System.out.println(pool.borrowObject());
      pool.borrowObject();
    }
  }
}

実用性ゼロなObjectPoolの動作確認するためだけのコードである。実行すると以下の出力をして無限待機する。

created:HpT2W
created:Yz1T5
created:oVjKr
oVjKr Yz1T5 HpT2W
oVjKr

プールの最大数3で4個目を取得しようとするので無限ブロックする。同時実行の要件次第で setMaxWait とかを設定する。

addObjectsはあらかじめプールにオブジェクト生成を命じるもの。これも要件次第で事前初期化か遅延かを選ぶ感じ。

createjavadocThis method must support concurrent, multi-threadedactivation.にある通り、マルチスレッド環境下では同期化の考慮が必要。

spring-bootのSOAP web serviceクライアント

Getting Started | Consuming a SOAP web serviceを基に、SOAP Webサービスから生成したクラスを使用してサービスにアクセスする。

事前準備

SOAP Webサービスは以前に作成したこちらを使用する。

kagamihoge.hatenablog.com

手順

gradle

https://start.spring.io/ でweb, lombok, devtoolsあたりを追加して生成し、それにGetting Started | Consuming a SOAP web serviceを参考に色々とSOAP関連の設定を追加する。

実際に使用する際は、ソースコード生成先・WSDLスキーマURL・package名、あたりを変更すると思われる。他はほぼコピペでOK。

plugins {
    id 'org.springframework.boot' version '2.7.1'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

configurations {
    jaxb
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

task genJaxb {
    ext.sourcesDir = "${buildDir}/generated-sources/jaxb"
    ext.classesDir = "${buildDir}/classes/jaxb"
    ext.schema = "http://localhost:8080/CalculatorService/Calculator?wsdl"

    outputs.dir classesDir

    doLast() {
        project.ant {
            taskdef name: "xjc", classname: "com.sun.tools.xjc.XJCTask",
                 classpath: configurations.jaxb.asPath
            mkdir(dir: sourcesDir)
            mkdir(dir: classesDir)

                xjc(destdir: sourcesDir, schema: schema,
                     package: "com.example.consumingwebservice.wsdl") {
                        arg(value: "-wsdl")
                    produces(dir: sourcesDir, includes: "**/*.java")
                }

                javac(destdir: classesDir, source: 1.8, target: 1.8, debug: true,
                     debugLevel: "lines,vars,source",
                     classpath: configurations.jaxb.asPath) {
                    src(path: sourcesDir)
                    include(name: "**/*.java")
                    include(name: "*.java")
                    }

                copy(todir: classesDir) {
                        fileset(dir: sourcesDir, erroronmissingdir: false) {
                        exclude(name: "**/*.java")
                }
            }
        }
    }
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    developmentOnly 'org.springframework.boot:spring-boot-devtools'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    
    implementation 'org.springframework.ws:spring-ws-core'
    // For Java 11:
    implementation 'org.glassfish.jaxb:jaxb-runtime'
    implementation(files(genJaxb.classesDir).builtBy(genJaxb))
    
    implementation 'javax.xml.soap:saaj-api:1.3.5'
    implementation 'com.sun.xml.messaging.saaj:saaj-impl:1.5.2'
    
    jaxb "com.sun.xml.bind:jaxb-xjc:2.1.7"
}

tasks.named('test') {
    useJUnitPlatform()
}

Getting Started | Consuming a SOAP web serviceに無いjavax.xml.soap:saaj-api, com.sun.xml.messaging.saaj:saaj-implは後述。

以下のようにgenJaxbタスクでWSDLからJavaソースコードとclassファイルが生成される。

gradlew genJaxb

application.properties

ローカルでSOAP webサービスを8080ポートで動かすのでクライアント側のwebは8081にしておく。

server.port=8081

java

サンプル実行用の適当なエンドポイントを作る。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@SpringBootApplication
public class SoapClientMain {
  @Autowired
  CalculatorClient client;

  @GetMapping("/sample")
  public void sample() {
    client.sum();
  }

  public static void main(String[] args) {
    SpringApplication.run(SoapClientMain.class, args);
  }
}

Jaxb2Marshallerとそれを使用するクライアントのbeanを定義する。実使用時にはクラス生成先のpackage名やWSDLのURLを変更する。

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.oxm.jaxb.Jaxb2Marshaller;

@Configuration
public class SoapClientConfig {
  @Bean
  public Jaxb2Marshaller marshaller() {
    Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
    // this package must match the package in the <generatePackage> specified in
    // pom.xml
    marshaller.setContextPath("com.example.consumingwebservice.wsdl");
    return marshaller;
  }

  @Bean
  public CalculatorClient client(Jaxb2Marshaller marshaller) {
    CalculatorClient client = new CalculatorClient();
    client.setDefaultUri("http://localhost:8080/CalculatorService/Calculator.wsdl");
    client.setMarshaller(marshaller);
    client.setUnmarshaller(marshaller);
    return client;
  }
}

クライアントクラスはWebServiceGatewaySupportを拡張し、そのクラスのメソッドを使用してSOAP Webサービスにアクセスする。

import javax.xml.bind.JAXBElement;

import org.springframework.ws.client.core.support.WebServiceGatewaySupport;

import com.example.consumingwebservice.wsdl.ObjectFactory;
import com.example.consumingwebservice.wsdl.Sum;
import com.example.consumingwebservice.wsdl.SumResponse;

public class CalculatorClient extends WebServiceGatewaySupport {

  public void sum() {
    ObjectFactory factory = new ObjectFactory();
    Sum r = factory.createSum();
    r.setArg0(334);
    r.setArg1(22);

    JAXBElement<Sum> request = factory.createSum(r);

    @SuppressWarnings("unchecked")
    JAXBElement<SumResponse> response = (JAXBElement<SumResponse>) getWebServiceTemplate()
        .marshalSendAndReceive("http://localhost:8080/CalculatorService/Calculator", request);
    System.out.println(response.getValue().getReturn());
  }
}

ハマった点

java.lang.ClassNotFoundException: javax.xml.soap.SOAPException

詳細は省略するがとにかくJava 17にはこのクラスは居なくなっている。なので、何らかの依存性を追加する必要がある。ここではjavax.xml.soap:saaj-api:1.3.5を追加しているが、おそらく他のやり方もあるように思われる(これ以上調べてない)。

Unable to create SAAJ meta-factoryProvider com.sun.xml.internal.messaging.saaj.soap.SAAJMetaFactoryImpl not found

Getting Startedには無いんだがcom.sun.xml.messaging.saaj:saaj-impl:1.5.2を追加しないと動かない。また、META-INF/services/javax.xml.soap.MetaFactoryでクラス名の指定の必要もある。

Getting Startedにはjavax.xml.soap:saaj-api, com.sun.xml.messaging.saaj:saaj-implが居ない。なので何等か俺の環境依存の問題があると思われるが、これ以上は調べていない。

Caused by: javax.xml.soap.SOAPException: Unable to create SAAJ meta-factoryProvider com.sun.xml.internal.messaging.saaj.soap.SAAJMetaFactoryImpl not found

色々ぐぐってみると、どうもsaaj-impl-x.x.x.jarのバージョンにってSAAJMetaFactoryImplのpackageが違うらしい。なのでMETA-INFで指定の必要がある。使用するsaaj-impl-x.x.x.jarの中身見るのが早いっぽい。

  • com.sun.xml.internal.messaging.saaj.soap.SAAJMetaFactoryImpl
  • com.sun.xml.messaging.saaj.soap.SAAJMetaFactoryImpl

Setting either 'contextPath', 'classesToBeBound', or 'packagesToScan' is required

Getting Started のConfiguring Web Service Componentsのサンプルコードのとおり、Jaxb2Marshaller#setContextPathで生成クラスのpackage名を指定する必要がある。

Caused by: java.lang.IllegalArgumentException: Setting either 'contextPath', 'classesToBeBound', or 'packagesToScan' is required

No marshaller registered

Getting Started のConfiguring Web Service Componentsのサンプルコードのとおり、WebServiceGatewaySupportを拡張したSOAPクライアント役のクラスにmarshallerをセットする必要がある。

Caused by: java.lang.IllegalStateException: No marshaller registered. Check configuration of WebServiceTemplate.

@XmlRootElement注釈がないため、……(以下略)

おそらくだが、このサンプルのSumはオブジェクトでは無いのでそのまま渡せずJAXBElement<Sum>でラップの必要がある、と思われる。spring-bootのサンプルコードとかはGetCountryRequestという感じで使ってるし。

Caused by: com.sun.istack.SAXException2: @XmlRootElement注釈がないため、タイプ"com.example.consumingwebservice.wsdl.Sum"を要素としてマーシャリングできません