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アプリケーションの中心部（プレゼンテーション層、ビジネスロジック層、インテグレーション層）を取り扱いました。
今回は、下記の赤枠で囲んである、例外処理、認証処理、ログを取り扱います。
![JerseySample-記事②.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3937984/8225fef4-222f-59f2-000e-598566caf2b8.jpeg)

前回記事はこちら↓↓↓

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

# 手順
①例外処理機能作成
②認証機能作成
③ログ出力機能作成

# ①例外処理機能作成
まずは、例外処理機能を作成します。

【実装方式】
JAX-RSのExceptionMapperの実装クラスを作成する方法で実装します。

【ExceptiomMapperの作成例】
以下が、JAX-RSランタイム中に発生したExceptionをキャッチするExceptionMapperの実装クラスの例です。
```
package com.example.exception.exceptionmapper;

import jakarta.inject.Inject;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ExceptionMapper;
import jakarta.ws.rs.ext.Provider;

import com.example.dto.ExceptionDto;
import com.example.exception.exceptionjson.ExceptionJson;
import com.example.util.Message;

@Provider
public class OtherException implements ExceptionMapper<Exception> {
	
	@Inject
	private ExceptionJson json;

	@Override
	public Response toResponse(Exception exception) {
		
		ExceptionDto dto = new ExceptionDto(); 
		dto.setCode(Message.CODE_500);
		dto.setMsg(Message.EXCEPTION_6);
		
		return Response.status(Response.Status.NOT_FOUND)
				.entity(json.exceptionJson(dto))
				.type(MediaType.APPLICATION_JSON)
				.build();
	}

}
```
ポイントは3つ。
1つ目は、@Providerアノテーションをクラスに付与していること。

2つ目は、ExceptionMapper<E>をimplementsすること。
（<E>の中身はキャッチしたい例外クラスを指定します。今回はすべての例外をキャッチできるよう、<Exception>にしています。）
```
@Provider
public class OtherException implements ExceptionMapper<Exception> {
```

3つ目は、ExceptionMapperのtoResponseメソッドをOverrideすること。
（Exception発生時のレスポンス内容をResponse型で返します。）
```
	@Override
	public Response toResponse(Exception exception) {
		
		ExceptionDto dto = new ExceptionDto(); 
		dto.setCode(Message.CODE_500);
		dto.setMsg(Message.EXCEPTION_6);
		
		return Response.status(Response.Status.NOT_FOUND)
				.entity(json.exceptionJson(dto))
				.type(MediaType.APPLICATION_JSON)
				.build();
	}

```

今回は、例外発生時のレスポンス形式を作るExceptionJson.java、ExceptionJson.javaの引数のためのDtoであるExceptionDto.java、Exception発生時のメッセージを定義しているMessage.javaを使用しています。
以下、3クラスの中身を記載いたします。

ExceptionJson.java
```
package com.example.exception.exceptionjson;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.ws.rs.InternalServerErrorException;

import com.example.dto.ExceptionDto;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

@ApplicationScoped
public class ExceptionJson {
	
	public String exceptionJson(ExceptionDto dto) {
		ObjectMapper mapper = new ObjectMapper();
		try {
			return mapper.writeValueAsString(dto);
		} catch (JsonProcessingException e) {
			throw new InternalServerErrorException();
		}
	}
	
}
```

ExceptionDto.java
```
package com.example.dto;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class ExceptionDto {
	
	private int code;
	private String msg;

}
```

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

public class Message {
	
	public static final int CODE_401 = 401;
	public static final int CODE_404 = 404;
	public static final int CODE_405 = 405;
	public static final int CODE_500 = 500;
	
	public static final String EXCEPTION_1 = "データが見つかりません";
	public static final String EXCEPTION_2 = "リソースが見つかりません";
	public static final String EXCEPTION_3 = "許可されていないHTTPメソッドです";
	public static final String EXCEPTION_4 = "データベースとの通信エラーです";
	public static final String EXCEPTION_5 = "SQLエラーです";
	public static final String EXCEPTION_6 = "その他のエラーです";
	public static final String EXCEPTION_7 = "認証に失敗しました";

}
```

今回は例として全ての例外をキャッチできるようにExceptionを指定したExceptionMapperの実装クラスを作成しました。
もしExceptionMapperを複数作成する場合は、子クラスから順に例外処理を行う仕様のようです。
例えば、RuntimeExceptionを指定したExceptionMapperの実装クラスとExceptionを指定したExceptionMapperの実装クラスを作成した場合、RuntimeExceptionとその子クラスで定義された例外が発生した場合、RuntimeExceptionを指定したExceptionMapperの実装クラスが起動し、それ以外の例外が発生した場合、Exceptionを指定したExceptionMapperの実装クラスが起動します。
（RuntimeExceptionはExceptionを間接的に継承している子クラスであるため。）

# ②認証機能作成
次に、認証機能を作成します。

【実装方式】
JAX-RSのContainerRequestFilterの実装クラスを作成して、ベーシック認証を実装します。

【作成例】
以下が、認証用に作成したContainerRequestFilterの実装クラスの例です。

AuthenticationFilter.java
```
package com.example.auth;

import java.io.IOException;
import java.util.Base64;
import java.util.ResourceBundle;
import java.util.StringTokenizer;

import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerRequestFilter;
import jakarta.ws.rs.ext.Provider;

import com.example.exception.UnAuthorizedException;

@Provider
public class AuthenticationFilter implements ContainerRequestFilter {
	
	//application.propertiesからユーザー名/パスワードを取得
	ResourceBundle rb = ResourceBundle.getBundle("application");
	String username = rb.getString("username");
	String password = rb.getString("password");
	
	//filter
	@Override
	public void filter(ContainerRequestContext requestContext) throws IOException {
		
		try {
			// Authorizationヘッダーから認証情報を取得 
			String authHeader = requestContext.getHeaderString("Authorization");
			
			// ベーシック認証情報をデコード 
			String encodedCredentials = authHeader.substring("Basic ".length()).trim(); 
			String credentials = new String(Base64.getDecoder().decode(encodedCredentials)); 
			
			// ユーザー名とパスワードを解析 
			StringTokenizer tokenizer = new StringTokenizer(credentials, ":"); 
			String getUsername = tokenizer.nextToken(); 
			String getPassword = tokenizer.nextToken();
			
			//認証処理
			if(!(username.equals(getUsername) && password.equals(getPassword))) {
				//認証失敗 の場合はUnAuthorizedExceptionをthrow
				throw new UnAuthorizedException(); 
			}
			
		}catch(Exception e){
			throw new UnAuthorizedException(); 
		}
		
	}

}
```
一つずつ確認します。

クラスには@Providerを付与し、ContainerRequestFilterをimplementsしています。
```
@Provider
public class AuthenticationFilter implements ContainerRequestFilter 
```

ユーザー名とパスワードはjava.util.ResourceBandleを用いてapplication.propertiesから取得しています。
```
	//application.propertiesからユーザー名/パスワードを取得
	ResourceBundle rb = ResourceBundle.getBundle("application");
	String username = rb.getString("username");
	String password = rb.getString("password");
```

application.propertiesには下記のようにユーザー名、パスワードを定義しています。
```
#認証情報
username=test
password=test
```

認証処理は、ContainerRequestFilterのfilterメソッドをOverrideして実装します。
```
	//filter
	@Override
	public void filter(ContainerRequestContext requestContext) throws IOException {
		
		try {
			// Authorizationヘッダーから認証情報を取得 
			String authHeader = requestContext.getHeaderString("Authorization");
			
			// ベーシック認証情報をデコード 
			String encodedCredentials = authHeader.substring("Basic ".length()).trim(); 
			String credentials = new String(Base64.getDecoder().decode(encodedCredentials)); 
			
			// ユーザー名とパスワードを解析 
			StringTokenizer tokenizer = new StringTokenizer(credentials, ":"); 
			String getUsername = tokenizer.nextToken(); 
			String getPassword = tokenizer.nextToken();
			
			//認証処理
			if(!(username.equals(getUsername) && password.equals(getPassword))) {
				//認証失敗 の場合はUnAuthorizedExceptionをthrow
				throw new UnAuthorizedException(); 
			}
			
		}catch(Exception e){
			throw new UnAuthorizedException(); 
		}
		
	}

```
クライアントはAuthorizationヘッダーにbase64方式でエンコードしたユーザー名：パスワードを付与してリクエストを送信することを想定しています。

認証方式は以下の順で行われます。
①Authorizationヘッダーから認証情報を取得
②認証情報をbase64方式でデコード
③ユーザー名とパスワードを取得
④定義されたユーザー名/パスワードと一致したら認証OK、一致しなかったらUnAuthorizedException（独自例外クラス/RuntimeExceptionを継承）をthrow

また、処理中に例外が発生した際も認証エラーとして処理できるよう、処理全体をtryブロックで囲み、catchブロックでUnAuthorizedExceptionをthrowするように記述しています。

# ③ログ出力機能作成
最後に、ログ出力機能を作成します。

【実装方式】
JAX-RSのContainerResponseFilterの実装クラスを作成して、レスポンスのステータスコードと、そのコードが200 OKならSUCCESS、それ以外ならERRORを標準出力する方式で実装します。
また、ログ出力にはorg.slf4jを使用します。

【作成例】
以下が、ログ出力用に作成したContainerResponseFilterの実装クラスの例です。

LoggingFilter.java
```
package com.example.log;

import java.io.IOException;

import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerResponseContext;
import jakarta.ws.rs.container.ContainerResponseFilter;
import jakarta.ws.rs.ext.Provider;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;

@Provider
public class LoggingFilter implements ContainerResponseFilter {
	
	private static final Logger logger = LoggerFactory.getLogger(LoggingFilter.class);

	//filter
	@Override
	public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext)
			throws IOException {
		
		// レスポンスステータスとメッセージをMDCに設定 
		MDC.put("status", String.valueOf(responseContext.getStatus()));
		if(String.valueOf(responseContext.getStatus()).equals("200")) {
			MDC.put("message", "SUCCESS");
		}else {
			MDC.put("message", "ERROR");
		}
		
		// ログ出力 
		logger.info(MDC.get("status"));
		logger.info(MDC.get("message"));
		
		// MDCをクリア 
		MDC.clear();
		
	}

	

}
```

一つずつ確認します。

クラスには@Providerを付与し、ContainerResponseFilterをimplementsしています。
```
@Provider
public class LoggingFilter implements ContainerResponseFilter {
```

また、フィールドとして、org.slf4j.Loggerを定義しています。
```
private static final Logger logger = LoggerFactory.getLogger(LoggingFilter.class);
```

ログ出力機能は、ContainerResponseFilterのfilterメソッドをOverrideして実装します。
```
	//filter
	@Override
	public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext)
			throws IOException {
		
		// レスポンスステータスとメッセージをMDCに設定 
		MDC.put("status", String.valueOf(responseContext.getStatus()));
		if(String.valueOf(responseContext.getStatus()).equals("200")) {
			MDC.put("message", "SUCCESS");
		}else {
			MDC.put("message", "ERROR");
		}
		
		// ログ出力 
		logger.info(MDC.get("status"));
		logger.info(MDC.get("message"));
		
		// MDCをクリア 
		MDC.clear();
		
	}
```

また、ログ形式を指定するlogback.xmlをsrc/main/resources配下に作成します。
```
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
	<appender name="console"
		class="ch.qos.logback.core.ConsoleAppender">
		<encoder>
			<pattern>%d{yyyy-MM-dd HH:mm:ss} %-5level %logger{36} [%X{status}] [%X{message}] - %msg%n</pattern>
		</encoder>
	</appender>
	<root level="debug">
		<appender-ref ref="console" />
	</root>
</configuration>
```

# 最後に
ここまでで、APIの基本的な機能に加え、例外処理、認証処理、ログ機能を実装することができました。
ご覧いただきありがとうございました。

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

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