0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

JAX-RS(Jersey)で作るRESTful API②

Last updated at Posted at 2025-01-04

やりたいこと

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

②でやること

前回は、REST APIアプリケーションの中心部(プレゼンテーション層、ビジネスロジック層、インテグレーション層)を取り扱いました。
今回は、下記の赤枠で囲んである、例外処理、認証処理、ログを取り扱います。
JerseySample-記事②.jpg

前回記事はこちら↓↓↓

手順

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

①例外処理機能作成

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

【実装方式】
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をimplementsすること。
(の中身はキャッチしたい例外クラスを指定します。今回はすべての例外をキャッチできるよう、にしています。)

@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にコミットしています。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?