#Spring BootでRest Apiを作る際の注意点
いろいろと苦労したので自分用のメモとして残す。
##①Restcontroller の戻り値で任意のクラスを使用してJsonで返す。その1
Spring Bootでは、JSONはJacksonで操作している。
Rest APIのリクエストでJSONを受け取る場合、RestControllerクラスのメソッドの引数にはJavaBeans(Entityクラス)やMapを指定できる。
また、レスポンスでJSONを返す際に、RestControllerクラスのメソッドの戻り値にJavaBeans(Entityクラス)やMapを使用できる。
この、JavaBeans(Entityクラス)やMapとJSONとの変換にJacksonが使われる。
そのためpom.xmlにJacksonの設定が必要。(jackson-coreとjackson-databindがあれば最低限動作する)
<!-- JSON -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>${jackson-version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>${jackson-version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson-version}</version>
</dependency>
##②Restcontroller の戻り値で任意のクラスを使用してJsonで返す。その2
DTOを作る際にはGetterとSetterを追加すること。
Lombokを使用する場合は@Dataアノテーションではなく
@Getter、@Setterアノテーションを使用すること。
理由は完全にわかっていないが、@DataアノテーションではGetterとSetterを判断できない模様。
Versionや設定によってはエラーにならないのかもしれない。
###OK 1:GetterとSetterをgetter、setterを自前で記載
public class Test {
private String test;
public Test(String test){}
public String getTest() {
return test;
}
public void setTest(String test) {
this.test = test;
}
}
###OK 2:@Getter、@Setterアノテーションで記載
@Getter
@Setter
public class Test {
private String test;
public Test(String test){}
}
###NG 1:@Dataアノテーションで記載
@Data
public class Test {
private String test;
public Test(String test){}
}
##③Restcontroller の戻り値で任意のクラスを使用してJsonで返す。その3
Restcontrollerで使用するproducesでコンテンツタイプをJsonで指定する。
producesを指定しない場合は暗黙的にJsonになるらしいのだが、
設定によってはXMLになるようで戻り値を変換する際にエラーになってしまう。
##任意の戻り値として使用するクラス。
@Getter
@Setter
public class Test {
private String test;
public Test(String test){}
}
###Jsonで返す-その①-Classをそのまま戻り値に設定
@RequestMapping(value="/xml", produces=MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public Test json() {
Test test = new Test();
test.setTest("OK");
return test;
}
//レスポンス->{ "test": "OK" }
###Jsonで返す-その②-ClassをResponseEntityに格納して戻り値に設定
戻り値の方が不定の場合は型引数にワイルドカードを意味する?を使用しResponseEntity>とクラスの戻り値の方に設定する。
@RequestMapping(value="/xml", produces=MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public ResponseEntity<Test> json() {
Test test = new Test();
test.setTest("OK");
return new ResponseEntity<>(test, HttpStatus.OK);
}
//レスポンス->{ "test": "OK" }
###XMLで返す。
@RequestMapping(value="/xml", produces=MediaType.APPLICATION_XML_VALUE)
@ResponseBody
public Test xml() {
Test test = new Test();
test.setTest("OK");
return test;
}
//レスポンス-><root><content>OK</content></root>
##④jacksonを使ってjsonをデシリアライズする-その1
オブジェクト内にクラスを定義するときはstaticなクラスにしないと落ちる
クラス内に変換先のオブジェクトをpublic classで書いたりするとエラーになる
public class Test {
private String test;
public Test(String test){}
public String getTest() {
return test;
}
public void setTest(String test) {
this.test = test;
}
}
//発生エラー
play.api.Application$$anon$1: Execution exception[[RuntimeException: org.codehaus.jackson.map.JsonMappingException: No suitable constructor found for type [simple type, class controllers.App$Test]: can not instantiate from JSON object (need to add/enable type information?) at [Source: N/A; line: -1, column: -1]]
##⑤jacksonを使ってjsonをデシリアライズする-その2
空入力のコンストラクタがないと怒られる
また、デシリアライズ先のオブジェクトに引数が空のコンストラクタを用意しないと怒られる。
public class Test {
private String test;
public Test(String test){}
public String getTest() {
return test;
}
public void setTest(String test) {
this.test = test;
}
}
##⑥jacksonを使ってjsonをデシリアライズする-その3
インスタンス化したObjectMapper以外の物を使うと超遅い
下記のようなやり方もあるが超遅い。一桁くらい処理時間に違いがある。
new ObjectMapper().readValue(json, Test.class);
Json.fromJson(json, Test.class);
下記のようにインスタンス化すればよい
ObjectNode node = Json.newObject();
Test test = Json.fromJson(node, Test.class);
処理速度を比較すると↓みたいな感じ。
ObjectMapper#readValue >> インスタンス化+ObjectMapper#readValue >= Json.fromJson
##⑦jacksonを使ってjsonをデシリアライズする-その4
Jackson JSON processorで存在しない変数を無視してマッピングする2つの方法
JacksonでJSON -> OBjectのマッピングをするとき、JSONには存在して、マッピング対象のクラスには存在しないフィールドがあると、
org.codehaus.jackson.map.exc.UnrecognizedPropertyException
が発生します。
###回避方法1:Annotation
マッピング対象のクラスに@JsonIgnorePropertiesをつける。
もしくは個別の項目に@JsonIgnoreをつける。
// クラス全体に設定する。Jsonデシリアライズ時に該当する項目をマッピングし、それ以外を自動的に無視する。
@JsonIgnoreProperties(ignoreUnknown = true)
public class Hoge {
public int id;
public String name;
public int age;
}
// クラス指定で任意のフィールドを変換の対象外にする
@JsonIgnoreProperties({"id", "age"})
public class Hoge {
public int id;
public String name;
public int age;
}
// 任意のフィールドを個別に変換の対象外にする
public class Hoge {
public int id;
@JsonIgnore
public String name;
@JsonIgnore
public int age;
}
###回避方法2:Configure
ObjectMapperにパラメータを設定する
// Jackson 1.9 and before
objectMapper.configure(DeserializationConfig.Feature.FAIL_ON_UNKNOWN_PROPERTIES, false);
// Jackson 2.0
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
##⑧RestTemplate でレスポンスの JSON とクラスのマッピングに失敗した場合に発生する例外
・org.springframework.web.client.RestClientException
RestClientException はサーバのエラーレスポンス、入出力エラー、レスポンスをデコードする際のエラーなどで発生する例外。
Base class for exceptions thrown by RestTemplate in case a request fails because of a server error response, as determined via ResponseErrorHandler.hasError(ClientHttpResponse), failure to decode the response, or a low level I/O error.
・org.springframework.http.converter.HttpMessageNotReadableException
親クラスの HttpMessageConversionException の説明によると変換に失敗した際に投げられる例外とのこと。
Thrown by HttpMessageConverter implementations when the HttpMessageConverter.read(java.lang.Class extends T>, org.springframework.http.HttpInputMessage) method fails.
・HttpMessageConversionException (Spring Framework 5.2.7.RELEASE API)
Thrown by HttpMessageConverter implementations when a conversion attempt fails.
・com.fasterxml.jackson.databind.exc.MismatchedInputException
マッピングするクラス定義に合わない JSON が来たときに発生する例外。
General exception type used as the base class for all JsonMappingExceptions that are due to input not mapping to target definition; these are typically considered "client errors" since target type definition itself is not the root cause but mismatching input. This is in contrast to InvalidDefinitionException which signals a problem with target type definition and not input.
This type is used as-is for some input problems, but in most cases there should be more explicit subtypes to use.
##⑨jacksonを使って総称型(Generic)のクラスをデシリアライズする。
Jackson 2.5以降であればTypeFactory.constructParametricType(Class parametrized、Class ... parameterClasses)メソッドで対応できます。
これによりJavaType、パラメーター化されたクラスとそのパラメーター化された型を指定することにより、Jacksonを厳密に定義できます。
デシリアライズしたい総称型がDataの場合
class Data <T> {
int found;
Class<T> hits;
}
ObjectMapper mapper = new ObjectMapper();
JavaType type = mapper.getTypeFactory().constructParametricType(Data.class, String.class);
Data<String> data = mapper.readValue(json, type);
クラスが複数のパラメーター化された型を宣言する場合
class Data <T, U> {
int found;
Class<T> hits;
List<U> list;
}
ObjectMapper mapper = new ObjectMapper();
JavaType type = mapper.getTypeFactory().constructParametricType(Data.class, String.class, Integer);
Data<String, Integer> data = mapper.readValue(json, type);
メソッド化するとこんな感じ
public static <T extends Serializable> BasicMessage<T> getConcreteMessageType(String jsonString, Class<T> classType) {
try {
ObjectMapper mapper = new ObjectMapper();
CollectionType javaType = mapper.getTypeFactory().constructParametricType(BasicMessage.class, classType);
return mapper.readValue(jsonString, javaType);
} catch (IOException e) {
}
}
##⑩SpringBootでのRestTemplateのタイムアウト設定
タイムアウトはHTTP通信に依存したものという判断か、RestTemplateにはタイムアウトを設定するようなメソッドはありません。
そのため個別のカスタマイズも簡単に適用することができるようになります。
方法の一つ目はRestTemplateBuilderを使用する方法です。
RestTemplateBuilderはSpring Boot 1.4から追加されたRestTemplateのBuilderクラスです。
import org.springframework.boot.web.client.RestTemplateBuilder;
RestTemplateBuilder restTemplateBuilder = new RestTemplateBuilder();
RestTemplate restTemplate = restTemplateBuilder
.setConnectTimeout(Duration.ofSeconds(3))
.setReadTimeout(Duration.ofSeconds(3))
.build();
SpringBootを使用しない場合はこんな感じ。
ClientRequestFactoryの実装で。
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
requestFactory.setConnectTimeout(3_000);
requestFactory.setReadTimeout(3_000);
RestTemplate restTemplate = new RestTemplate(requestFactory);