springのWebClientでXMLレスポンスを受信


# サンプルコード

```gradle:build.gradle
plugins {
	id 'java'
	id 'org.springframework.boot' version '3.3.4'
	id 'io.spring.dependency-management' version '1.1.6'
}

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

java {
	toolchain {
		languageVersion = JavaLanguageVersion.of(17)
	}
}

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.springframework.boot:spring-boot-starter-webflux'
	implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml'
	implementation("org.glassfish.jaxb:jaxb-runtime")

	compileOnly 'org.projectlombok:lombok'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testImplementation 'io.projectreactor:reactor-test'
	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

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

以下はXMLレスポンスを格納するクラス。注意点として、```@XmlRootElement```の指定が必要。

```java
import jakarta.xml.bind.annotation.XmlRootElement;
import lombok.Data;

@Data

@XmlRootElement(name = "note")
public class WebClientSampleDto {
  String to;
  String from;
}

```

```java
public class SampleMain {

  public static void main(String[] args) {
    ExchangeStrategies strategies = ExchangeStrategies.builder()
        .codecs(conf -> conf.defaultCodecs().jaxb2Decoder(new Jaxb2XmlDecoder()))
        .build();

    WebClient client = WebClient.builder()
        .exchangeStrategies(strategies)
        .build();

    WebClientSampleDto body = client.get()
        .uri("http://localhost:3002/xml")
        .retrieve()
        .bodyToMono(WebClientSampleDto.class)
        .block();

    System.out.println(body);
  }
}

```

ほぼ見た目通り。デフォルトコーデックにXML処理用の```Jaxb2XmlDecoder```を指定する。今回はレスポンスだけなのでdecoderの指定だけだが、リクエストは```jaxb2Encoder```を指定すれば良いと思われる。


# 詳細
## 動作確認
適当なモックサーバ、今回は https://mockoon.com/ を使用し、URL```http://localhost:3002/xml```が```Content-Type```は```application/xml```で以下を返すように設定する。

```xml
<note>
  <to>to-hoge</to>
  <from>from-kagami</from>
</note>
```

これを実行すると以下の結果が得られる。

```
WebClientSampleDto(to=to-hoge, from=from-kagami)
```

## 対応Content-Typeの変更
```Jaxb2XmlDecoder```のデフォルト対応```Content-Type```は、```application/xml```, ```text/xml```, ```application/*+xml```、となっている。なのでこれ以外に対応したい場合は別途以下のように設定の必要がある。具体的には```Jaxb2XmlDecoder```のコンストラクタに指定する。以下は```application/octet-stream```に対応させている。

```java
import org.springframework.util.MimeType;
import org.springframework.util.MimeTypeUtils;

    ExchangeStrategies strategies = ExchangeStrategies.builder()
        .codecs(conf -> conf.defaultCodecs().jaxb2Decoder(
            new Jaxb2XmlDecoder(MimeTypeUtils.APPLICATION_OCTET_STREAM)))
        .build();
```

# はまった点
## UnsupportedMediaTypeException
この例外はクラス名が示す通り、レスポンスに対応するデコーダーがWebClientに未設定なことを示す。ただ、その原因は様々なケースがありうる。

```java
Caused by: org.springframework.web.reactive.function.UnsupportedMediaTypeException: Content type 'application/octet-stream;charset=utf-8' not supported for bodyType=org.example.app.WebClientSampleDto
	at org.springframework.web.reactive.function.BodyExtractors.readWithMessageReaders(BodyExtractors.java:205)
 ```

 まず、```jaxb2Decoder```が未指定だと当然ながらこの例外をスローする。

 また、Content-Typeが未対応のものだとこれも例外をスローする。

 デコーダはJAXBであってjacksonではない。よって、XML用アノテーションは```@XmlRootElement```を使用する必要がある。```@JacksonXmlRootElement```ではない。これも例外をスローする。

 # 参考文献
 - https://stackoverflow.com/questions/73187642/how-to-retrieve-xml-using-webclient-in-spring
 - https://stackoverflow.com/questions/64587787/parse-text-html-response-to-xml-with-web-client-in-spring-boot
 - https://github.com/FasterXML/jackson-dataformat-xml/issues/260
 - https://github.com/spring-projects/spring-framework/issues/20256