最近行儀の悪い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()
を修正することになるイケてない作り。
なので次回はもう少し汎用的な作りに変えてみます。