やりたいこと
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)
①でやること
下記の赤枠で囲んである、REST APIアプリケーションの中心部(プレゼンテーション層、ビジネスロジック層、インテグレーション層)を取り扱います。
(例外処理、認証処理、ログはJAX-RS(Jersey)で作るRESTful API②で取り扱います。)
手順
①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の定義です。
【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クラスを作成します。
詳しくは、下記記事をご参照くださいませ。
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経由で取得する点に留意してください。
詳しくは、下記記事をご参照くださいませ。
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の基本的な機能を実装することができました。
次回は例外処理、認証処理、ログ機能を実装します。
続きはこちらから↓↓↓
ソースコード
上記サンプルコードはGithubにコミットしています。