やりたいこと
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の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にコミットしています。