JAX-RS（Jersey）で作るRESTful API①

# やりたいこと
JakartaEEのAPIを使ってRESTful APIを開発したい。

# 前提
【機能要件】
・データを全件JSON形式で取得できるGETメソッドのREST API
・データを1件指定してJSON形式で取得できるPOSTメソッドのREST API

【主な使用技術】
・Java17
・Apache Maven
・Apache Tomcat10
・PostgreSQL

【実装方式】
・プレゼンテーション層はJAX-RS（Jersey）
・ビジネスロジック層はCDI
・インテグレーション層はMyBatis
・JAX-RSランタイム中で発生したExceptionは適宜処理（ExceptionMapper）
・リクエスト到達時にユーザー名/パスワードで認証処理（ContainerRequestFilter）
・レスポンス返却時にログを記録（ContainerResponseFilter）

以上を図式化したものが下記です。
![JerseySample-全体.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3937984/fde0c726-d818-cb27-daa7-d5ac22b9e3d1.jpeg)

# ①でやること
下記の赤枠で囲んである、REST APIアプリケーションの中心部（プレゼンテーション層、ビジネスロジック層、インテグレーション層）を取り扱います。
（例外処理、認証処理、ログはJAX-RS（Jersey）で作るRESTful API②で取り扱います。）
![JerseySample-記事①.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3937984/8d749bf5-e73a-d8f1-7871-4f3766e8b0f5.jpeg)

# 手順
①pom.xml/web.xml/beans.xml作成
②Dto/Dao作成
③Service作成
④Application/Resource作成

# ①pom.xml/web.xml/beans.xml作成
【pom.xml作成】
まずはpom.xmlを作成します。
下記依存をdependenciesに追加します。
```
		<!-- JAX-RS API -->
		<dependency>
			<groupId>jakarta.ws.rs</groupId>
			<artifactId>jakarta.ws.rs-api</artifactId>
			<version>4.0.0</version>
		</dependency>

		<!-- CDI API -->
		<dependency>
			<groupId>jakarta.enterprise</groupId>
			<artifactId>jakarta.enterprise.cdi-api</artifactId>
			<version>4.1.0</version>
		</dependency>

		<!-- MyBatis -->
		<dependency>
			<groupId>org.mybatis</groupId>
			<artifactId>mybatis</artifactId>
			<version>3.5.17</version>
		</dependency>

		<!-- MyBatis-CDI -->
		<dependency>
			<groupId>org.mybatis</groupId>
			<artifactId>mybatis-cdi</artifactId>
			<version>2.1.0</version>
		</dependency>

		<!-- Lombok -->
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<version>1.18.36</version>
			<scope>provided</scope>
		</dependency>

		<!-- PostgreSQL Driver -->
		<dependency>
			<groupId>org.postgresql</groupId>
			<artifactId>postgresql</artifactId>
			<version>42.7.4</version>
		</dependency>

		<!--Weld Servlet Core-->
		<dependency>
			<groupId>org.jboss.weld.servlet</groupId>
			<artifactId>weld-servlet-core</artifactId>
			<version>6.0.0.CR2</version>
		</dependency>

		<!--Jersey Core Server-->
		<dependency>
			<groupId>org.glassfish.jersey.core</groupId>
			<artifactId>jersey-server</artifactId>
			<version>4.0.0-M1</version>
		</dependency>

		<!--Jersey Container Servlet-->
		<dependency>
			<groupId>org.glassfish.jersey.containers</groupId>
			<artifactId>jersey-container-servlet</artifactId>
			<version>4.0.0-M1</version>
		</dependency>

		<!--Jersey Inject HK2 -->
		<dependency>
			<groupId>org.glassfish.jersey.inject</groupId>
			<artifactId>jersey-hk2</artifactId>
			<version>4.0.0-M1</version>
		</dependency>

		<!--Jersey Ext Cdi1x -->
		<dependency>
			<groupId>org.glassfish.jersey.ext.cdi</groupId>
			<artifactId>jersey-cdi1x</artifactId>
			<version>4.0.0-M1</version>
		</dependency>

		<!--Jersey Media JSON Jackson-->
		<dependency>
			<groupId>org.glassfish.jersey.media</groupId>
			<artifactId>jersey-media-json-jackson</artifactId>
			<version>4.0.0-M1</version>
		</dependency>

		<!--Jackson Databind-->
		<dependency>
			<groupId>com.fasterxml.jackson.core</groupId>
			<artifactId>jackson-databind</artifactId>
			<version>2.18.2</version>
		</dependency>

		<!--org.slf4j-->
		<dependency>
			<groupId>org.slf4j</groupId>
			<artifactId>slf4j-api</artifactId>
			<version>2.0.16</version>
		</dependency>

		<!--logback-classic-->
		<dependency>
			<groupId>ch.qos.logback</groupId>
			<artifactId>logback-classic</artifactId>
			<version>1.5.15</version>
		</dependency>
```
内容は、JAX-RS、CDI、MyBatis関連、Lombok、PostgreSQL Driver、Weld Servlet、Jersey関連、Jackason、slf4j/logback等ログ関連です。
必要に応じてテストフレームワークやAPI Client フレームワークを追加します。

【web.xml作成】
src/main/webapp/WEB-INF配下にweb.xmlを作成します。
今回はweb.xmlに記述する内容は特にないので、下記のように特に中身は記述しません。
```
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
                      http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
    version="3.0" metadata-complete="true">
    
</web-app>
```

【beans.xml作成】
次に、src/main/webapp/WEB-INF配下（web.xmlと同じディレクトリ）にbeans.xmlを作成します。
JavaEE7以降はbeans.xmlを省略してもCDIが有効化されるようですが、省略するとなぜか動かなかったので一応追加いたします。
```
<beans xmlns="http://xmlns.jcp.org/xml/ns/javaee"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd"
	version="1.1" bean-discovery-mode="all">
</beans>
```

# ②Dto/Dao作成
【テーブル定義】
Dto/Daoを作成するにあたり、テーブル定義を確認します。
下記が今回使用するtestdbの定義です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3937984/2e5d9621-677d-9275-9a8a-3b389b645934.png)

【Dto作成】
上記テーブル定義に合わせてDtoを作成します。
```
package com.example.dto;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class SampleDto {
	
	private int id;
	private String msg;

}
```

【Dao作成】
次にDaoを作成します。
今回はMyBatisを使用するため、Mapperを作成します。
```
package com.example.dao;

import java.util.List;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import com.example.dto.SampleDto;

@Mapper
public interface SampleMapper {
	
	List<SampleDto> getList();
	
	SampleDto getById(@Param("id") int id);

}
```
次に実際にSQLを記述するmapper.xmlを作成します。
（今回はsrc/mai/resources配下に作成します。）
クエリの内容は、今回はデータ全件取得とidを指定して1件だけ取得するものとします。
```
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.example.dao.SampleMapper">

	<select id="getList" parameterType="string" resultType="com.example.dto.SampleDto"> 
		SELECT 
		id, 
		msg 
		FROM 
		testdb.testdb;
	</select>
	
	<select id="getById" parameterType="int" resultType="com.example.dto.SampleDto"> 
		SELECT 
		id, 
		msg 
		FROM 
		testdb.testdb
		WHERE
		id = #{id};
	</select>
		
</mapper>
```
次に、MapperをCDI経由でインジェクションするためのmybatis-config.xmlとSqlSessionFactoryProducerクラスを作成します。
詳しくは、下記記事をご参照くださいませ。

https://qiita.com/hiroto_mis/items/d3de5bde4d1b635350c4

mybatis-config.xml（src/mai/resources配下）
```
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
  PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-config.dtd">

<configuration>
	<environments default="test">
		<environment id="test">
			<transactionManager type="JDBC" />
			<dataSource type="POOLED">
				<property name="driver" value="org.postgresql.Driver" />
				<property name="url" value="" />
				<property name="username" value="" />
				<property name="password" value="" />
			</dataSource>
		</environment>
	</environments>
	<mappers>
		<mapper resource="./mapper.xml" />
	</mappers>
</configuration>
```

SqlSessionFactoryProducer.java
```
package com.example.util;

import java.io.IOException;
import java.io.InputStream;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.context.Dependent;
import jakarta.enterprise.inject.Produces;

import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.mybatis.cdi.SessionFactoryProvider;

@Dependent
public class SqlSessionFactoryProducer {
	
	@Produces
	@ApplicationScoped
	@SessionFactoryProvider
	public SqlSessionFactory produceFactory() throws IOException{
		InputStream fileStream = Resources.getResourceAsStream("mybatis-config.xml");
		return new SqlSessionFactoryBuilder().build(fileStream);
	}

}
```

# ③Service作成
次に、Serviceクラスを作成します。
今回は特にビジネスロジック層で必要な処理はないため、インテグレーション層からデータを取得しプレゼンテーション層にデータを横流しするのみとします。
ただし、MapperをSqlSession経由で取得する点に留意してください。
詳しくは、下記記事をご参照くださいませ。

https://qiita.com/hiroto_mis/items/d3de5bde4d1b635350c4

SampleService.java
```
package com.example.service;

import java.util.List;

import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Inject;

import org.apache.ibatis.session.SqlSession;

import com.example.dao.SampleMapper;
import com.example.dto.SampleDto;

@RequestScoped
public class SampleService {
	
	@Inject
	private SqlSession sqlSession;
	
	
	public List<SampleDto> getList(){
		 return sampleMapper().getList();
	}
	
	public SampleDto getById(int id) {
		return sampleMapper().getById(id);
	}
	
	private SampleMapper sampleMapper() {
		return sqlSession.getMapper(SampleMapper.class);
	}

}
```

# ④Application/Resource作成
【Aplicationサブクラス作成】
まずはjakarta.ws.rs.core.Applicationを継承したサブクラスを作成します。
ポイントは2つ。
1つ目は、jakarta.ws.rs.core.Applicationを継承すること。
2つ目は、ApplicationPathを指定することです。
これにより、ApplicatioPath以下にアクセスした際、Resource等JAX-RSで使用するファイルをJAX-RSランタイムに通知することができます。
（web.xmlに記述する方法でもOKです。）

ApplicationConfig.java
```
package com.example.application;

import jakarta.ws.rs.ApplicationPath;
import jakarta.ws.rs.core.Application;

@ApplicationPath("rest")
public class ApplicationConfig extends Application {

}
```
【リクエストボディのデータを格納するクラス作成】
今回、POST形式のAPIはリクエストボディからデータを取得しJavaクラスにバインドする仕様で作成するため、リクエストボディのデータを格納するクラスを作成します。
```
package com.example.model;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class Input {
	
	private int id;

}
```

【Resource作成】
最後にResourceクラスを作成します。
以下が今回作成したResourceクラスの全体です。
```
package com.example.resource;

import java.util.List;

import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;

import com.example.dto.SampleDto;
import com.example.exception.DataNotFoundException;
import com.example.model.Input;
import com.example.service.SampleService;

@Path("api")
@RequestScoped
public class SampleResource {
	
	@Inject
	private SampleService service;	
	
	
	@Path("getlist")
	@Produces(MediaType.APPLICATION_JSON)
	@GET
	public List<SampleDto> getList(){
		return service.getList();
	}
	
	@Path("getbyid")
	@Produces(MediaType.APPLICATION_JSON)
	@Consumes(MediaType.APPLICATION_JSON)
	@POST
	public SampleDto getById(Input input) {
		SampleDto dto = service.getById(input.getId());
		if(dto == null) {
			throw new DataNotFoundException();
		}else {
			return dto;
		}
	}

}
```
まずはクラスに付与しているアノテーションに関して。
```
@Path("api")
@RequestScoped
public class SampleResource {
```
本クラスで提供するリソースへアクセスするためのパスは、ApplicationPathと合わせてrest/api～となります。
また、CDIのスコープはリクエストスコープとして定義します。

次に、データ全件取得をGETで提供するメソッドに関して。
```
	@Path("getlist")
	@Produces(MediaType.APPLICATION_JSON)
	@GET
	public List<SampleDto> getList(){
		return service.getList();
	}
```
@GETによりGETメソッドで実行することができます。
また@Pathにより、rest/api/getlistで本リソースにアクセスすることができます。
また、@ProducesをJSONで定義することにより、レスポンス形式をJSON指定しています。

次に、データ1件取得をPOSTで提供するメソッドに関して。
```
	@Path("getbyid")
	@Produces(MediaType.APPLICATION_JSON)
	@Consumes(MediaType.APPLICATION_JSON)
	@POST
	public SampleDto getById(Input input) {
		SampleDto dto = service.getById(input.getId());
		if(dto == null) {
			throw new DataNotFoundException();
		}else {
			return dto;
		}
	}
```
@POSTによりPOSTメソッドで実行することができます。
また@Pathにより、rest/api/getbyidで本リソースにアクセスすることができます。
また、@ConsumesをJSONで定義することにより、JSON形式のリクエストボディからデータを取得し、引数のInput.javaにバインドすることができます。（SpringBootの@RequestBodyのような挙動になります。）
さらに、@ProducesをJSONで定義することにより、レスポンス形式をJSON指定しています。

# 実行
以上の作業ののち、作成したアプリケーションをビルドし、APサーバー（今回はTomcat10）にデプロイしてサーバーを起動します。
その後、URIを叩き、想定通りのレスポンスが返ってくれば完了です。
（今回作成したAPIのGETメソッドの場合、http://FQDN/コンテキストパス/rest/api/getlist）

# 最後に
ここまでで、APIの基本的な機能を実装することができました。
次回は例外処理、認証処理、ログ機能を実装します。
続きはこちらから↓↓↓

https://qiita.com/hiroto_mis/items/9c5c9ec512ee6107658a

# ソースコード
上記サンプルコードはGithubにコミットしています。

https://github.com/SatoHiroto526/JerseySample/tree/master