Edited at

行儀の悪いAPIのレスポンスを自前のPOJOにバインドする(1)

More than 3 years have passed since last update.

最近行儀の悪いAPIを使う機会がありました。

こんな感じ。


  • Request

GET http://[APIのドメイン]/user/1


  • Response

Content-Type: text/plain;charset=Windows31J

Response-Body: User_ID=1&User_Name=ジョン&User_Birthday=19900613


  • バインドしたいPOJO

public class User {

private Long id;
private String name;
private LocalDate birthday;

// getter and setter //
}

う〜ん。どうしよう。


普通に取得してセットしてみる

SpringのRestTemplate使って普通にAPI叩いてレスポンスを取得してみる。

&で区切られてるし、ここはFormHttpMessageConverterを使うか。

FormHttpMessageConverter converter = new FormHttpMessageConverter();

// text/plainで返ってくるからtext/plainを追加
converter.setSupportedMediaTypes(Arrays.asList(MediaType.APPLICATION_FORM_URLENCODED, MediaType.TEXT_PLAIN));
// charsetも指定
converter.setCharset(Charset.forName("Windows-31J"));

RestTemplate restTemplate = new RestTemplate()
restTemplate.setMessageConverters(Arrays.asList(converter));

// ここまではクラスの初期化時に実行してクラス変数に持っていても大丈夫

MultiValueMap<String, String> response = restTemplate.getForObject(apiUrl, MultiValueMap.class);

User user = new User();
user.setId(Long.valueOf(response.getFirst("User_ID")));
user.setName(response.getFirst("User_Name"));
user.setBirthday(LocalDate.parse(response.getFirst("User_Birthday"), DateTimeFormatter.ofPattern("yyyyMMdd")));

return user;

※ Nullチェックやってません。

長っ。。。

RestTemplate#getForObject()からだとしても、API叩いてレスポンスの値をバインドしてって、ここでのコードの役割多すぎるし見通し悪い。

Userクラスがまだ3つしかフィールドが無いからまだ良いけどこれが10とか20になってきたらノイズだらけで見れたものじゃない。

一番重要な役割であるAPI叩くコードが目立たない。

そこでHttpMessageConverterっていう名前があるのなら「お前がやれよ」と思い、こいつにバインドさせることにした。


HttpMessageConverterにバインドさせてみる

この部分をHttpMessageConverterに移動させたい

User user = new User();

user.setId(Long.valueOf(response.getFirst("User_ID")));
user.setName(response.getFirst("User_Name"));
user.setBirthday(LocalDate.parse(response.getFirst("User_Birthday"), DateTimeFormatter.ofPattern("yyyyMMdd")));

基本的な機能はFormHttpMessageConverterの機能を使いたいからこれも使う。

ただし、FormHttpMessageConverterの処理に影響を与えたくないので、継承じゃなくてラップして使うことにする。

public class UserFormHttpMessageConverter implements HttpMessageConverter<User> {

private FormHttpMessageConverter formHttpMessageConverter;

public UserFormHttpMessageConverter(FormHttpMessageConverter formHttpMessageConverter) {
this.formHttpMessageConverter = formHttpMessageConverter;
}

@Override
public boolean canRead(Class<?> clazz, MediaType mediaType) {
// UserクラスだったらOK。MediaTypeの判定はめんどくさいから
// FormHttpMessageConverterにやらせる。(clazzはMultiValueMapで来たと嘘をつく。)
return User.class.isAssignableFrom(clazz) && formHttpMessageConverter.canRead(MultiValueMap.class, mediaType);
}

@Override
public User read(Class<? exntends User> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
@SuppressWarnings("unchecked")
final MultiValueMap<String, String> response = formHttpMessageConverter.read((Class<? extends MultiValueMap<String, ?>>) MultiValueMap.class, inputMessage);

User user = new User();
user.setId(Long.valueOf(response.getFirst("User_ID")));
user.setName(response.getFirst("User_Name"));
user.setBirthday(LocalDate.parse(response.getFirst("User_Birthday"), DateTimeFormatter.ofPattern("yyyyMMdd")));

return user;
}

@Override
public List<MediaType> getSupportedMediaTypes() {
return formHttpMessageConverter.getSupportedMediaTypes();
}

// 以下サポートしない

@Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
throw new UnsupportedOperationException();
}

@Override
public void write(User request, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
throw new UnsupportedOperationException();
}
}

これでUserクラスをバインドできる。

FormHttpMessageConverter converter = new FormHttpMessageConverter();

// text/plainで返ってくるからtext/plainを追加
converter.setSupportedMediaTypes(Arrays.asList(MediaType.APPLICATION_FORM_URLENCODED, MediaType.TEXT_PLAIN));
// charsetも指定
converter.setCharset(Charset.forName("Windows-31J"));

RestTemplate restTemplate = new RestTemplate();
// カスタムHttpMessageConverterでラップして設定
restTemplate.setMessageConverters(Arrays.asList(new UserFormHttpMessageConverter(converter)));

ここまでが初期化処理。

// カスタムHttpMessageConverterでラップして設定。

restTemplate.setMessageConverters(Arrays.asList(new UserFormHttpMessageConverter(converter)));

ここが変わった行。

で、次が取得処理。

User user = restTemplate.getForObject(apiUrl, User.class);

return user; // User(id=1, name=ジョン, birthday=1990-06-13)

うん。すっきり。


次回予告

だけどUserクラスに特化したHttpMessageConverterがキモくて仕方ない。

しかもUserクラスのフィールド増えたら随時UserFormHttpMessageConverter#read()を修正することになるイケてない作り。

なので次回はもう少し汎用的な作りに変えてみます。

次回:行儀の悪いAPIのレスポンスを自前のPOJOにバインドする(2)