SpringFramework + Jersey2構成でAPIを構築する
これは何
SpringFramework + Jersey2でAPIを作ります。
想定読者
- Java書いたことがある。
- SpringFramework はだいたい雰囲気わかる。
- Jersey2に興味がある。(Jerseyは3がそろそろ出ます)
業務でJersey2で書かれたアプリケーションの改修する必要があるんだけどJerseyって何って人
Jerseyとは
そもそものところからですが,JerseyとはJAX-RS (Java API for RESTful Web Services)のリファレンス実装です。
(※リファレンス実装という単語がわかりにくいと思うのですが,「APIの仕様に準拠している実装例の1つ」という認識で問題ないかと思います。)
Jersey自体はそろそろ3が出そうですが今回扱うのは2になります。
SpringFramework + Jersey2
環境
- SpringFramework 5.3.1
- Jersey 2.32
- Apache Maven 3.6.3
コンパイルし,warでtomcatあたりにデプロイする想定です。
SpringBootは使いません,基本xml設定を書きます。
依存ライブラリ
以下をpom.xmlに追加しました。
SpringFramework
特に書くことがないFrameworkです
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>${spring.version}</version>
</dependency>
Jersey
Jerseyの本体です。
<dependency>
<groupId>org.glassfish.jersey.containers</groupId>
<artifactId>jersey-container-servlet</artifactId>
<version>${jersey2.version}</version>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.core</groupId>
<artifactId>jersey-server</artifactId>
<version>${jersey2.version}</version>
</dependency>
jersey-bean-validation
JerseyにRequestが渡される前にBean-Validationを実行するのに必要です。
<dependency>
<groupId>org.glassfish.jersey.ext</groupId>
<artifactId>jersey-bean-validation</artifactId>
<version>${jersey2.version}</version>
</dependency>
jersey-media-json-jackson
Jerseyでjsonを扱えるようにします。
<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-json-jackson</artifactId>
<version>${jersey2.version}</version>
</dependency>
jersey-spring5
JerseyとSpringで連携させる際に必要です。
本来JerseyにはHK2 というコンテナもあるのですが,Springを使用するので追加します。
<dependency>
<groupId>org.glassfish.jersey.ext</groupId>
<artifactId>jersey-spring5</artifactId>
<version>${jersey2.version}</version>
</dependency>
lombok
特筆することはないただのlombokです。
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.16</version>
<scope>provided</scope>
</dependency>
中身
Sample001.java
Jerseyのエンドポイントを定義します。
@javax.ws.rs.Path
valueとしてエンドポイントを指定します。
クラス,メソッドの両方に付与することができます。
その場合 クラスのPath+メソッドのPath というurlになります。
パスにパラメータを設定することが可能ですが,ここでは記述しません。
@javax.ws.rs.POST,@GET,@PUT...
HTTPのMethodを指定します
@javax.ws.rs.Consumes
requestで受け取るデータの形式を指定します。
@javax.ws.rs.Produces
responseで返却するデータの形式を指定します。
@javax.validation.Valid
今回入力値チェックにBeanValidationを使用するので,引数のオブジェクトに設定しています。
@Path("/sample001")
public interface Sample001 {
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response getsample001(@Valid Sample001Form form);
}
Sample001Form.java
Formオブジェクトです。
BeanValidationを使用するのでフィールドにアノテーションを付与しています。
今回は書いていませんがネストしたオブジェクトを持ち,内部でもバリデーションが必要な場合はフィールドに**@Valid**アノテーションを付与します。
@Data
public class Sample001Form {
@NotNull
private int id;
@NotBlank
private String name;
}
Sample001Impl.java
Sample001.javaでエンドポイントを定義したので実装クラスを作成します。
こちらは特に特殊なことは行っておらず,Requestをラップして返却します。
jersey-media-json-jacksonを依存に含めているので,**entity()**メソッドへMapオブジェクトを渡した場合,自動的にjsonへ変換されResponseが作成されます。
@Component
public class Sample001Impl implements Sample001 {
@Override
public Response getsample001(Sample001Form form) {
Map<String, Object> map = new HashMap<>();
map.put("request", form);
return Response.ok().entity(map).build();
}
}
ConstraintViolationExceptionMapper.java
今回BeanValidationを使用しているのでRequestに問題があった場合の処理を書きます。
こちらでExceptionMapperを用意しない場合はtomcatのデフォルトのHttpStatus 400エラーを返します。
しかし異常なリクエストの場合にもログを出力したいなどの要件があることがあるので,こちらで定義します。
ConstraintViolationException
BeanValidation異常時にthrowされる例外です。
異常の情報は持っていますが,リクエスト関連の情報は持っていません。
ログ出力の要件でリクエスト情報が必要な場合は@Context アノテーションでHttpServletRequest型をインジェクトしてください。
(その際にBodyを取得しようとすると(getReader,getInputStream等)既に呼ばれており,もう呼べないといった内容の例外を吐きますので,HttpServletRequestを拡張したクラスなどでキャッシュ機構を作り,Filterなどでキャストしておく必要があります。)
@Provider
@Component
public class ConstraintViolationExceptionMapper implements ExceptionMapper<ConstraintViolationException> {
@Override
public Response toResponse(ConstraintViolationException exception) {
return Response.status(Response.Status.BAD_REQUEST).entity(prepareMessage(exception)).type("text/plain")
.build();
}
private String prepareMessage(ConstraintViolationException exception) {
StringBuilder message = new StringBuilder();
for (ConstraintViolation<?> cv : exception.getConstraintViolations()) {
message.append(cv.getPropertyPath() + " " + cv.getMessage() + " " + cv.getConstraintDescriptor() + "\n");
}
return message.toString();
}
}
web.xml
jerseyのサーブレットを登録する方法はいくらかありますが,今回はweb.xmlを使用する方法を採用しています。
servletの部分で,jerseyであること,対象クラスをパッケージ指定すること,対象パッケージを指定しています。
今回はcom.example.demo.controllerの階層にエンドポイントを定義したinterfaceを配置します。
com.example.demo.exception.handlerには例外ハンドラを配置しています。
<web-app>
<display-name>Spring Jersey Sample Application</display-name>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<servlet>
<servlet-name>jersey-serlvet</servlet-name>
<servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class>
<init-param>
<param-name>jersey.config.server.provider.packages</param-name>
<param-value>com.example.demo.controller;com.example.demo.exception.handler</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>jersey-serlvet</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
</web-app>
applicationContext.xml
特筆すべき事はありません。
com.example.demoパッケージ以下をスキャンしています。
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd">
<context:component-scan base-package="com.example.demo" />
</beans>
動作確認
mvn packageでwarを生成し,tomcatにデプロイを行い動作確認をします。
クライアントとしてVSCodeのREST Clientを使用します。
urlの先頭に**/demo**がついているのはwarの名前をそのままエンドポイントの前につけているtomcatの設定です。
正常リクエスト
POST http:/demo/sample001 HTTP/1.1
content-type: application/json
{
"id": 1,
"name": "name"
}
HTTP/1.1 200
Content-Type: application/json
Content-Length: 34
Date: Thu, 10 Dec 2020 21:21:24 GMT
Connection: close
{
"request": {
"id": 1,
"name": "name"
}
}
異常リクエスト
nameが空文字
POST http:/demo/sample001 HTTP/1.1
content-type: application/json
{
"id": 1,
"name": ""
}
HTTP/1.1 400
Content-Type: text/plain
Content-Length: 478
Date: Thu, 10 Dec 2020 21:40:41 GMT
Connection: close
getsample001.arg0.name must not be blank
ConstraintDescriptorImpl{annotation=j.v.c.NotBlank,
payloads=[],
hasComposingConstraints=true,
isReportAsSingleInvalidConstraint=false,
constraintLocationKind=FIELD,
definedOn=DEFINED_LOCALLY,
groups=[interface javax.validation.groups.Default],
attributes={groups=[Ljava.lang.Class;@419a7a90,
message={javax.validation.constraints.NotBlank.message},
payload=[Ljava.lang.Class;@3908e0d7
},
constraintType=GENERIC,
valueUnwrapping=DEFAULT
}
(改行を加えています)
異常リクエスト2
idを削除
POST http:/demo/sample001 HTTP/1.1
content-type: application/json
{
"name": "name"
}
HTTP/1.1 200
Content-Type: application/json
Content-Length: 34
Date: Thu, 10 Dec 2020 21:48:54 GMT
Connection: close
{
"request": {
"id": 0,
"name": "name"
}
}
正常レスポンスが帰って来てしまいました。
ここから推測なのですが,オブジェクト作成→ マッピング→BeanValidationで行うためintの初期値が入ってしまい,そもそもnullという状況はありえない。
ということでint→Integerに変更し挑戦。
HTTP/1.1 400
Content-Type: text/plain
Content-Length: 473
Date: Thu, 10 Dec 2020 21:56:01 GMT
Connection: close
getsample001.arg0.id must not be null
ConstraintDescriptorImpl{
annotation=j.v.c.NotNull,
payloads=[],
hasComposingConstraints=true,
isReportAsSingleInvalidConstraint=false,
constraintLocationKind=FIELD,
definedOn=DEFINED_LOCALLY,
groups=[interface javax.validation.groups.Default],
attributes={
groups=[Ljava.lang.Class;@388c3cb3,
message={javax.validation.constraints.NotNull.message},
payload=[Ljava.lang.Class;@2b68db1a
},
constraintType=GENERIC,
valueUnwrapping=DEFAULT
}
想定通りのレスポンス
おわり
一旦jsonでのrequest,responseを行い,requestにBeanValidationをかけハンドリングまで実装できたので終わりとします。
intでnullが入らないのはうっかりしてました。
今回のコードです。
参考サイト
Discussion