インターセプタ
インターセプタとは?
例えば『Edge』専用にしたいアプリにアクセスする際には、当然ブラウザ判定を入れてアクセス許可の判断をする必要があります。
これを全バッキングビーンに仕込むのは面倒ですよね。 そこで登場するのが『インターセプタ』です。
前述で横取り処理と言った様に、画面にアクセスした際に本来コンストラクタが処理されるタイミングで処理を奪います。
そこでブラウザ判定のチェックを入れればスッキリしますよね。 それでは依存関係の整理から始めましょう。
<!-- Jakarta Interceptor API -->
<dependency>
<groupId>jakarta.interceptor</groupId>
<artifactId>jakarta.interceptor-api</artifactId>
<version>2.2.0</version>
<scope>provided</scope>
</dependency>
インターセプタバインディング作成
サクッと以下を作成してください。
package annotation;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.annotation.ElementType;
import jakarta.interceptor.InterceptorBinding;
@Inherited
@InterceptorBinding
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
public @interface Logged {}
package interceptor;
import java.io.Serializable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import annotation.Logged;
import jakarta.annotation.Priority;
import jakarta.interceptor.AroundInvoke;
import jakarta.interceptor.Interceptor;
import jakarta.interceptor.InvocationContext;
@Interceptor
@Logged
@Priority(Interceptor.Priority.APPLICATION)
public class LoggingInterceptor implements Serializable {
private static final long serialVersionUID = 1L;
private static final Logger logger = LoggerFactory.getLogger(LoggingInterceptor.class);
@AroundInvoke
public Object logMethod(InvocationContext context) throws Exception {
Class<?> targetClass = context.getMethod().getDeclaringClass();
String className = targetClass.getSimpleName();
String methodName = context.getMethod().getName();
try {
logger.info("「【LoggingInterceptor】{}.{}」の処理を開始します。", className, methodName);
// 本来の処理(ターゲットメソッド)を実行
Object result = context.proceed();
logger.info("「【LoggingInterceptor】{}.{}」の処理を終了します。", className, methodName);
return result;
} catch (Exception e) {
System.out.println("[ERROR] 例外発生: " + e.getMessage());
logger.info("「【LoggingInterceptor】{}.{}」の処理で【例外】が発生しました。。", className, methodName);
throw e;
}
}
}
Object result = context.proceed(); つまりこの処理の前後に『奪いたい処理』を記述する訳です。
以上で設定は終了です。 それではこの『LoggingInterceptor』の影響を受けるクラスを指定してみましょう。
ここでは『SampleBean』にアノテーションを付与してみます。
~ 省略 ~
@Logged
@Named
@ViewScoped
public class SampleBean extends BaseBean {
~ 省略 ~
どうですか? インターセプタからのログが出力されましたね。
適当なメソッドを発火させてみて下さい。
更に大量のログ。。 って本当に大量のログが出力されてませんか?
実は『バッキングビーンあるある』なんですが、画面と連動してるクラスだけに『DTO』を管理してるんですよね。 画面を描画する際にDTOの各プロパティを呼んでUIの初期値を取得しますよね。
この呼び出し方は当然『getHoge()』的なメソッドで取得する訳です。 そうです。クラスに『インターセプタ』を設定すると言う事は、全てのメソッドに対して発火するのです!
これは実は、かなり深刻な問題です。
有事の際にログを整理したつもりが『インターセプタのログで溢れている』では本末転倒ですよね。 ではどうするか?
一応対応策としては、クラスにインターセプタを付与するのを止めて『各メソッドに付与する』方法があります。
でもちょっとウザいですよね。 ひと癖はありますが、以下のコードを『インターセプタ』に追加してみて下さい。
~ 省略 ~
@AroundInvoke
public Object logMethod(InvocationContext context) throws Exception {
Class<?> targetClass = context.getMethod().getDeclaringClass();
String className = targetClass.getSimpleName();
String methodName = context.getMethod().getName();
// 「get」「set」「is」が最初に付くメソッドは無視する
if (methodName.startsWith("get") || methodName.startsWith("set") || methodName.startsWith("is")) {
return context.proceed();
}
try {
logger.info("「【LoggingInterceptor】{}.{}」の処理を開始します。", className, methodName);
// 本来の処理(ターゲットメソッド)を実行
Object result = context.proceed();
logger.info("「【LoggingInterceptor】{}.{}」の処理を終了します。", className, methodName);
return result;
} catch (Exception e) {
System.out.println("[ERROR] 例外発生: " + e.getMessage());
logger.info("「【LoggingInterceptor】{}.{}」の処理で【例外】が発生しました。。", className, methodName);
throw e;
}
}
~ 省略 ~
ウザいDTOのログが消えたハズです。
ユーザーエージェントを判別するインターセプタ
では実際に作ってみましょうw
package annotation;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.annotation.ElementType;
import jakarta.interceptor.InterceptorBinding;
@Inherited
@InterceptorBinding
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
public @interface UserAgentChecked {}
package interceptor;
import java.io.Serializable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import annotation.UserAgentChecked;
import jakarta.annotation.Priority;
import jakarta.interceptor.AroundInvoke;
import jakarta.interceptor.Interceptor;
import jakarta.interceptor.InvocationContext;
@Interceptor
@UserAgentChecked
@Priority(Interceptor.Priority.APPLICATION)
public class UserAgentInterceptor implements Serializable {
private static final long serialVersionUID = 1L;
private static final Logger logger = LoggerFactory.getLogger(UserAgentInterceptor.class);
@AroundInvoke
public Object logMethod(InvocationContext context) throws Exception {
logger.info("「【UserAgentInterceptor】の処理を開始します。");
try {
// 本来の処理(ターゲットメソッド)を実行
Object result = context.proceed();
return result;
} catch (Exception e) {
System.out.println("[ERROR] 例外発生: " + e.getMessage());
logger.info("【UserAgentInterceptor】の処理で【例外】が発生しました。");
throw e;
}
}
}
これを以下の様に修正します。
package interceptor;
import java.io.Serializable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import annotation.UserAgentChecked;
import jakarta.annotation.Priority;
import jakarta.inject.Inject;
import jakarta.interceptor.AroundInvoke;
import jakarta.interceptor.Interceptor;
import jakarta.interceptor.InvocationContext;
import jakarta.servlet.http.HttpServletRequest;
@Interceptor
@UserAgentChecked
@Priority(Interceptor.Priority.APPLICATION)
public class UserAgentInterceptor implements Serializable {
private static final long serialVersionUID = 1L;
private static final Logger logger = LoggerFactory.getLogger(UserAgentInterceptor.class);
@Inject
private HttpServletRequest request;
@AroundInvoke
public Object logMethod(InvocationContext context) throws Exception {
logger.info("「【UserAgentInterceptor】の処理を開始します。");
String deviceType = "";
String osType = "";
String browserType = "";
if (request != null) {
// User-Agent ヘッダーの取得
String userAgent = request.getHeader("User-Agent");
if (userAgent != null) {
deviceType = parseDeviceType(userAgent);
osType = parseOs(userAgent);
browserType = parseBrowser(userAgent);
}
}
logger.info("【UserAgentInterceptor】アクセスしたデバイスは「{}」です。", deviceType);
logger.info("【UserAgentInterceptor】アクセスしたOSは「{}」です。", osType);
logger.info("【UserAgentInterceptor】アクセスしたブラウザは「{}」です。", browserType);
try {
// 本来の処理(ターゲットメソッド)を実行
Object result = context.proceed();
return result;
} catch (Exception e) {
logger.info("【UserAgentInterceptor】の処理で【例外】が発生しました。");
throw e;
}
}
// ざっくりとデバイスの種類を判定
private String parseDeviceType(String ua) {
ua = ua.toLowerCase();
if (ua.contains("googlebot") || ua.contains("bingbot") || ua.contains("crawler")) {
return "BOT";
} else if (ua.contains("ipad") || (ua.contains("android") && !ua.contains("mobile"))) {
return "TABLET";
} else if (ua.contains("iphone") || ua.contains("ipod") || (ua.contains("android") && ua.contains("mobile"))) {
return "SP";
} else {
return "PC";
}
}
// OSを判定
private String parseOs(String ua) {
ua = ua.toLowerCase();
if (ua.contains("iphone") || ua.contains("ipad") || ua.contains("ipod")) {
return "iOS";
} else if (ua.contains("android")) {
return "Android";
} else if (ua.contains("windows nt")) {
return "Windows";
} else if (ua.contains("mac os x") || ua.contains("macintosh")) {
return "macOS";
} else if (ua.contains("linux")) {
return "Linux";
} else if (ua.contains("cros")) {
return "ChromeOS";
}
return "Unknown OS";
}
// ブラウザを判定
private String parseBrowser(String ua) {
ua = ua.toLowerCase();
// 判定順序に注意(Edge/Opera/SamsungはChromeの文字列も含むため先に判定)
if (ua.contains("edg/") || ua.contains("edge/")) {
return "Edge";
} else if (ua.contains("opr/") || ua.contains("opera")) {
return "Opera";
} else if (ua.contains("samsungbrowser")) {
return "Samsung Internet";
} else if (ua.contains("trident/") || ua.contains("msie")) {
return "Internet Explorer";
} else if (ua.contains("chrome") || ua.contains("crios")) { // CriOS = iOS版Chrome
return "Chrome";
} else if (ua.contains("firefox") || ua.contains("fxios")) { // FxIos = iOS版Firefox
return "Firefox";
} else if (ua.contains("safari")) {
return "Safari";
}
return "Unknown Browser";
}
}
特にまだ弾く処理等は入れていませんが、挙動だけは確認できます。
ユーザーエージェントの解析で気になる処理があったと思います。
そう以下2点ですね
悪質なボット等で意識的にユーザーエージェントを隠ぺいしている場合もありますが『セキュリティ・プライバシー保護設定』で取得できないケースもあります。 基本的にユーザーエージェントはツールなどを使い、いくらでも詐称できるので、厳密な判定には向きません。
あくまで『性善説が前提の処理』である意識は持ちましょう。
ユーザーエージェントの判定なんてセッションが発行される前の1回で十分ですよね。
しかしそこまで範囲を狭めると、何処に設定すれば良いか分からなくなります。 そこで画面に最初にGET通信したとき(要するにビュースコープが外れている場合)に発火させるのがベターな設定でしょう。
もうお分かりですね。そう。
『コンストラクタ』です。 ただしここにも落とし穴が。。
一度『コンストラクタ』の章を一読してみて下さい。 それでは改めて何処にインターセプタを設置するか。
一見各バッキングビーンの『initialize()』が良いと思い浮かべると思います。
しかし、実はここにインターセプタを設定しても『発火しません』。 インターセプタの発火の条件は『直接呼び出された場合』となります。
つまり自身のクラス内で『hoge() ⇒ foo() ⇒ bar()』と呼び出しても『hoge()』メソッドのみ発火する事になります。
よって『BaseBean.init()』メソッド内から呼び出された『initialize()』メソッドは無視されるのです。
なら『init()』にインターセプタを設置するしかありませんが、ここはBaseBeanの『isFirstTiem』フラグの制御外なのでイベント単位で発火します。 これは『詰んだ』と思っているあなた。甘いです。方法はまだあります。それは。
『インターセプタ内から「BaseBean」の《isFirstTiem》の状態を参照する』です! まずは『BaseBean』に外から『isFirstTiem』の状態を参照できるメソッドを作成します。
package view;
import java.io.Serializable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import annotation.UserAgentChecked;
public abstract class BaseBean implements Serializable {
private static final long serialVersionUID = 1L;
private static final Logger logger = LoggerFactory.getLogger(BaseBean.class);
protected boolean isFirstTiem = false;
protected static final String SELF = null;
@UserAgentChecked
public void init() {
if (isFirstTiem) {
return;
}
logger.info("BaseBeanを開始します。");
// 子がオーバーライドする
initialize();
// ライフサイクル中は初期化しない
isFirstTiem = true;
}
// 子の初期化メソッド定義
protected abstract void initialize();
public boolean isFirstTiem() {
return isFirstTiem;
}
}
このタイミングでインターセプタも『init()』に設定しちゃいましょう。 次に『UserAgentInterceptor』を修正します。
package interceptor;
import java.io.Serializable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import annotation.UserAgentChecked;
import jakarta.annotation.Priority;
import jakarta.inject.Inject;
import jakarta.interceptor.AroundInvoke;
import jakarta.interceptor.Interceptor;
import jakarta.interceptor.InvocationContext;
import jakarta.servlet.http.HttpServletRequest;
import view.BaseBean;
@Interceptor
@UserAgentChecked
@Priority(Interceptor.Priority.APPLICATION)
public class UserAgentInterceptor implements Serializable {
private static final long serialVersionUID = 1L;
private static final Logger logger = LoggerFactory.getLogger(UserAgentInterceptor.class);
@Inject
private HttpServletRequest request;
@AroundInvoke
public Object logMethod(InvocationContext context) throws Exception {
// ターゲットクラスを取得
Object target = context.getTarget();
// ターゲットクラスが「BaseBean」の場合
if (target instanceof BaseBean) {
// ターゲットインスタンスを「BaseBean」型にキャストする
BaseBean baseBean = (BaseBean) target;
// ここで「isFirstTiem」の内容を確認できます
if (!baseBean.isFirstTiem()) {
logger.info("「【UserAgentInterceptor】の処理を開始します。");
String deviceType = "";
String osType = "";
String browserType = "";
if (request != null) {
// User-Agent ヘッダーの取得
String userAgent = request.getHeader("User-Agent");
if (userAgent != null) {
deviceType = parseDeviceType(userAgent);
osType = parseOs(userAgent);
browserType = parseBrowser(userAgent);
}
}
logger.info("【UserAgentInterceptor】アクセスしたデバイスは「{}」です。", deviceType);
logger.info("【UserAgentInterceptor】アクセスしたOSは「{}」です。", osType);
logger.info("【UserAgentInterceptor】アクセスしたブラウザは「{}」です。", browserType);
}
}
try {
// 本来の処理(ターゲットメソッド)を実行
Object result = context.proceed();
return result;
} catch (Exception e) {
logger.info("【UserAgentInterceptor】の処理で【例外】が発生しました。");
throw e;
}
}
// ざっくりとデバイスの種類を判定
private String parseDeviceType(String ua) {
ua = ua.toLowerCase();
if (ua.contains("googlebot") || ua.contains("bingbot") || ua.contains("crawler")) {
return "BOT";
} else if (ua.contains("ipad") || (ua.contains("android") && !ua.contains("mobile"))) {
return "TABLET";
} else if (ua.contains("iphone") || ua.contains("ipod") || (ua.contains("android") && ua.contains("mobile"))) {
return "SP";
} else {
return "PC";
}
}
// OSを判定
private String parseOs(String ua) {
ua = ua.toLowerCase();
if (ua.contains("iphone") || ua.contains("ipad") || ua.contains("ipod")) {
return "iOS";
} else if (ua.contains("android")) {
return "Android";
} else if (ua.contains("windows nt")) {
return "Windows";
} else if (ua.contains("mac os x") || ua.contains("macintosh")) {
return "macOS";
} else if (ua.contains("linux")) {
return "Linux";
} else if (ua.contains("cros")) {
return "ChromeOS";
}
return "Unknown OS";
}
// ブラウザを判定
private String parseBrowser(String ua) {
ua = ua.toLowerCase();
// 判定順序に注意(Edge/Opera/SamsungはChromeの文字列も含むため先に判定)
if (ua.contains("edg/") || ua.contains("edge/")) {
return "Edge";
} else if (ua.contains("opr/") || ua.contains("opera")) {
return "Opera";
} else if (ua.contains("samsungbrowser")) {
return "Samsung Internet";
} else if (ua.contains("trident/") || ua.contains("msie")) {
return "Internet Explorer";
} else if (ua.contains("chrome") || ua.contains("crios")) { // CriOS = iOS版Chrome
return "Chrome";
} else if (ua.contains("firefox") || ua.contains("fxios")) { // FxIos = iOS版Firefox
return "Firefox";
} else if (ua.contains("safari")) {
return "Safari";
}
return "Unknown Browser";
}
}
各バッキングビーンの『initialize()』が発火するタイミングでしかインターセプタも発火しませんよね。 最後に実際に特定のデバイスを弾いてみましょう。
デバイスの設定はどこでするか? そんなの当然『appinfo.properties』ですよね。
※『動的環境設定』参照 『src/main/resources/META-INF/develop/appinfo.properties』にデバイスの許可リストを追記します。
ENABLE_DEVICE=PC
ENABLE_OS=Windows
ENABLE_BROWSER=Edge|Chrome
※ルールとして複数設定する場合は『|(パイプ)』で繋ぐ様にします それでは『UserAgentInterceptor』を修正してみましょう。
package interceptor;
import java.io.Serializable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import annotation.UserAgentChecked;
import config.AppConfig;
import jakarta.annotation.Priority;
import jakarta.inject.Inject;
import jakarta.interceptor.AroundInvoke;
import jakarta.interceptor.Interceptor;
import jakarta.interceptor.InvocationContext;
import jakarta.servlet.http.HttpServletRequest;
import view.BaseBean;
@Interceptor
@UserAgentChecked
@Priority(Interceptor.Priority.APPLICATION -10)
public class UserAgentInterceptor implements Serializable {
private static final long serialVersionUID = 1L;
private static final Logger logger = LoggerFactory.getLogger(UserAgentInterceptor.class);
@Inject
private HttpServletRequest request;
@AroundInvoke
public Object logMethod(InvocationContext context) throws Exception {
// ターゲットクラスを取得
Object target = context.getTarget();
// ターゲットクラスが「BaseBean」の場合
if (target instanceof BaseBean) {
// ターゲットインスタンスを「BaseBean」型にキャストする
BaseBean baseBean = (BaseBean) target;
// ここで「isFirstTiem」の内容を確認できます
if (!baseBean.isFirstTiem()) {
logger.info("「【UserAgentInterceptor】の処理を開始します。");
if (request != null) {
// User-Agent ヘッダーの取得
String userAgent = request.getHeader("User-Agent");
if (userAgent != null) {
String deviceType = parseDeviceType(userAgent);
String osType = parseOs(userAgent);
String browserType = parseBrowser(userAgent);
// デバイス判定
if (!checkType(deviceType, "ENABLE_DEVICE")) {
throw new RuntimeException("【" + deviceType + "】は利用できません。");
}
// OS判定
if (!checkType(osType, "ENABLE_OS")) {
throw new RuntimeException("【" + deviceType + "】は利用できません。");
}
// ブラウザ判定
if (!checkType(browserType, "ENABLE_BROWSER")) {
throw new RuntimeException("【" + browserType + "】は利用できません。");
}
}
}
}
}
try {
// 本来の処理(ターゲットメソッド)を実行
Object result = context.proceed();
return result;
} catch (Exception e) {
logger.info("【UserAgentInterceptor】の処理で【例外】が発生しました。");
throw e;
}
}
// ざっくりとデバイスの種類を判定
private String parseDeviceType(String ua) {
ua = ua.toLowerCase();
if (ua.contains("googlebot") || ua.contains("bingbot") || ua.contains("crawler")) {
return "BOT";
} else if (ua.contains("ipad") || (ua.contains("android") && !ua.contains("mobile"))) {
return "TABLET";
} else if (ua.contains("iphone") || ua.contains("ipod") || (ua.contains("android") && ua.contains("mobile"))) {
return "SP";
} else {
return "PC";
}
}
// OSを判定
private String parseOs(String ua) {
ua = ua.toLowerCase();
if (ua.contains("iphone") || ua.contains("ipad") || ua.contains("ipod")) {
return "iOS";
} else if (ua.contains("android")) {
return "Android";
} else if (ua.contains("windows nt")) {
return "Windows";
} else if (ua.contains("mac os x") || ua.contains("macintosh")) {
return "macOS";
} else if (ua.contains("linux")) {
return "Linux";
} else if (ua.contains("cros")) {
return "ChromeOS";
}
return "Unknown OS";
}
// ブラウザを判定
private String parseBrowser(String ua) {
ua = ua.toLowerCase();
// 判定順序に注意(Edge/Opera/SamsungはChromeの文字列も含むため先に判定)
if (ua.contains("edg/") || ua.contains("edge/")) {
return "Edge";
} else if (ua.contains("opr/") || ua.contains("opera")) {
return "Opera";
} else if (ua.contains("samsungbrowser")) {
return "Samsung Internet";
} else if (ua.contains("trident/") || ua.contains("msie")) {
return "Internet Explorer";
} else if (ua.contains("chrome") || ua.contains("crios")) { // CriOS = iOS版Chrome
return "Chrome";
} else if (ua.contains("firefox") || ua.contains("fxios")) { // FxIos = iOS版Firefox
return "Firefox";
} else if (ua.contains("safari")) {
return "Safari";
}
return "Unknown Browser";
}
// 「appinfo」で指定した環境以外の場合『false』を返却する
private boolean checkType(String type, String appKey) {
String[] enableList = AppConfig.get(appKey, "").split("\\|");
for (String enable : enableList) {
if (enable.equals(type)) {
return true;
}
}
return false;
}
}
『appinfo』で設定していないユーザーエージェントは例外が送出されますね。
処理自体はフレームワークの技術的な事はなく、単純にループ処理しているだけなので割愛します。
シレっと『@Priority(Interceptor.Priority.APPLICATION -10) 』となっている事に気づきましたか?
実はこれインターセプタの発火の順番を指定できるんです。
先ほどまで『LoggingInterceptor』が先に発火していました。
でもユーザーエージェントで弾かれる場合、もうログには興味ありませんよね?
そしてその設定が『Priority.APPLICATION -10』となるわけです。 この定数は整数なので、普通に四則演算できます。
そしてこの数値が小さいほどインターセプタは先に発火するのです。





この『DTO』のログを非表示にする方法は単純に、メソッド名の最初の文字列に「get」「set」「is」を評価してるだけです。
ログって軽視されがち(レビューでログが無いなんて指摘受けませんよね)ですけど、実はシステムの生命線だと個人的には思います。つまり『getSelectUserByName』的なメソッドも無視されてしまいます。
一番安全なのは当然各メソッドにアノテーションを付与する方法ですが、メソッドの『命名規則』をプロジェクトで徹底する事で回避する事も可能です。