AOPの基本概念と重要性
アスペクト指向プログラミング(AOP)はJavaだけでなく、他の言語でも利用されています。例えば、ASP.NETのパイプラインモデルにおいても、認証やログ出力などの共通処理を横断的に処理するために利用可能です。本記事では、AOPの具体的な仕組みや用語の説明は一旦置いておき、導入の背景と実装方法に焦点を当てます。
横断的処理と縦断的処理
まず、「横断」と「縦断」の違いについて理解しましょう。例として、手術の切り方ではなく、システム設計における処理の切り分けを考えてみます。ある機能を横断的に適用するとは、複数のモジュールにまたがって共通して処理を適用することを意味します。一方、縦断的処理は、特定のビジネスロジックに直接関連する処理です。
AOPの必要性
本番環境での問題は、UATやST環境では検出できないケースがあります。そのため、ログ出力を通じて問題を追跡できるようにすることが一般的です。しかし、すべてのメソッドにログ出力や例外キャッチ処理を手動で追加すると、コードの修正箇所が多くなり、保守性が低下します。
このような状況において、AOPの「横断的処理」の考え方が有効です。AOPでは、複数のクラスにまたがる共通処理(例:ログ出力、トランザクション制御、セキュリティ制御)を再利用可能なモジュールとして扱い、モジュール間の結合度を下げます。
コードでのAOP実装
以下では、サービスメソッドの前後にログを出力し、例外をキャッチする処理をAOP的に実装する方法を見ていきます。
インターフェースと実装クラス
まず、基本的なインターフェースとその実装クラスを定義します。
package com.example.service;
public interface Service {
void execute(String name);
}
package com.example.service.impl;
import com.example.service.Service;
public class ServiceImpl implements Service {
@Override
public void execute(String name) {
System.out.println("ServiceImpl execute: " + name);
}
}
静的プロキシによる実装
静的プロキシでは、共通処理をプロキシクラスに集約します。
package com.example.proxy;
import com.example.service.Service;
public class StaticServiceProxy implements Service {
private Service target;
public StaticServiceProxy(Service target) {
this.target = target;
}
@Override
public void execute(String name) {
System.out.println("Before execution");
try {
target.execute(name);
} catch (Exception e) {
System.err.println("Exception caught: " + e.getMessage());
} finally {
System.out.println("After execution");
}
}
}
動的プロキシによる実装
動的プロキシを利用すると、インターフェースに依存せず、より柔軟な共通処理を実装できます。
package com.example.proxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class DynamicServiceProxy implements InvocationHandler {
private Object target;
public DynamicServiceProxy(Object target) {
this.target = target;
}
public Object getProxy() {
return Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
this
);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("Before invoking method: " + method.getName());
try {
return method.invoke(target, args);
} catch (Exception e) {
System.err.println("Error in method: " + method.getName() + ", " + e.getMessage());
throw e;
} finally {
System.out.println("After invoking method: " + method.getName());
}
}
}
ログ出力の抽象化
ログ出力先がコンソールからファイルやデータベースに変わった場合を想定し、ログ処理をインターフェースで抽象化します。
package com.example.logging;
import java.lang.reflect.Method;
public interface Logger {
void logStart(Method method);
void logEnd(Method method);
}
package com.example.logging.impl;
import java.lang.reflect.Method;
import java.time.LocalDateTime;
public class ConsoleLogger implements Logger {
@Override
public void logStart(Method method) {
System.out.println(LocalDateTime.now() + " - Start method: " + method.getName());
}
@Override
public void logEnd(Method method) {
System.out.println(LocalDateTime.now() + " - End method: " + method.getName());
}
}
動的プロキシへのログ抽象化適用
先ほどの動的プロキシにログ出力の抽象化を組み込みます。
package com.example.proxy;
import com.example.logging.Logger;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class EnhancedDynamicProxy implements InvocationHandler {
private Object target;
private Logger logger;
public EnhancedDynamicProxy(Object target, Logger logger) {
this.target = target;
this.logger = logger;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
logger.logStart(method);
try {
return method.invoke(target, args);
} catch (Exception e) {
System.err.println("Error occurred: " + e.getMessage());
throw e;
} finally {
logger.logEnd(method);
}
}
}
実行例
public class Main {
public static void main(String[] args) {
Service service = new ServiceImpl();
Logger logger = new ConsoleLogger();
Service proxy = (Service) Proxy.newProxyInstance(
service.getClass().getClassLoader(),
service.getClass().getInterfaces(),
new EnhancedDynamicProxy(service, logger)
);
proxy.execute("test");
}
}