kagamihogeの日記

kagamihogeの日記です。

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"を要素としてマーシャリングできません