開発環境
- Java 1.8.0
- Spring Boot 2.0.2.RELEASE
- Spring core 5.0.6.RELEASE
- Spring aop 5.0.6.RELEASE
背景
Serviceの実装
以下のような特に変哲もないToStringService
インタフェースと、その実装クラスであるToStringServiceImpl
があるとします。
public interface SimpleService {
void foo();
}
import org.springframework.stereotype.Service;
@Service
public class SimpleServiceImpl implements SimpleService {
@Override
public void foo() {
System.out.println("foo");
}
@Override
public String toString() {
return this.getClass().getSimpleName();
}
}
実行するまでもないですが、SimpleServiceImpl
のtoString()
を実行すると"SimpleServiceImpl"が出力されます。
プロキシ化する
ProxyConfig
を継承し、任意のクラスをプロキシ化するクラスを作成します。名前は適当です。
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.springframework.aop.Advisor;
import org.springframework.aop.framework.ProxyConfig;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.util.ClassUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
public class ToStringProxy<T> extends ProxyConfig {
private MultiValueMap<Class<?>, Advisor> advisorMap = new LinkedMultiValueMap<>();
public T apply(T target) {
List<Advisor> advisors = findAdvisors(target.getClass());
return createProxy(target, advisors);
}
public void addAdvisor(Class<?> target, Advisor... advisors) {
for (Advisor advisor : advisors) {
this.advisorMap.add(target, advisor);
}
}
@SuppressWarnings("unchecked")
private T createProxy(T target, List<Advisor> advisors) {
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.setTarget(target);
proxyFactory.addAdvisors(advisors);
if (!isProxyTargetClass()) {
proxyFactory.setInterfaces(ClassUtils.getAllInterfaces(target));
}
proxyFactory.setProxyTargetClass(isProxyTargetClass());
proxyFactory.setExposeProxy(isExposeProxy());
proxyFactory.setFrozen(isFrozen());
proxyFactory.setOpaque(isOpaque());
proxyFactory.setOptimize(isOptimize());
return (T) proxyFactory.getProxy();
}
private List<Advisor> findAdvisors(Class<?> targetClass) {
for (Map.Entry<Class<?>, List<Advisor>> entry : advisorMap.entrySet()) {
if (entry.getKey().isAssignableFrom(targetClass)) {
return entry.getValue();
}
}
return Collections.emptyList();
}
}
次にSimpleService
をプロキシ化するアドバイザクラスを作成します。
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.aop.aspectj.AspectJExpressionPointcutAdvisor;
import org.springframework.stereotype.Component;
@Component
public class ToStringAdvisor {
private ToStringProxy<SimpleService> proxy;
public ToStringAdvisor() {
proxy = new ToStringProxy<>();
// メソッドtoString()を対象とする
String expression = "execution(* " + SimpleService.class.getName() + ".toString())";
AspectJExpressionPointcutAdvisor pointcutAdvisor = new AspectJExpressionPointcutAdvisor();
pointcutAdvisor.setAdvice(new ToStringInterceptor());
pointcutAdvisor.setExpression(expression);
proxy.addAdvisor(SimpleService.class, pointcutAdvisor);
}
public SimpleService apply(SimpleService target) {
return proxy.apply(target);
}
private static class ToStringInterceptor implements MethodInterceptor {
private ToStringInterceptor() {}
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
return "proxied: ".concat(String.valueOf(invocation.proceed()));
}
}
}
※サンプルなので入力チェックとか省いています。
SimpleServiceImpl
のtoString()
が呼ばれると"proxied: SimpleServiceImpl"と出力される想定です。
テストしてみる
では、さっそくToStringInterceptor
に書いた処理が呼ばれるかテストしてみます。
import static org.hamcrest.CoreMatchers.equalTo;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
public class ProxySampleApplicationTests {
@Autowired
SimpleService service;
@Autowired
ToStringAdvisor adviser;
@Test
public void proxiedToStringTest() {
service = adviser.apply(service);
Assert.assertThat(service.toString(), equalTo("proxied: SimpleServiceImpl"));
}
}
結果は……
java.lang.AssertionError:
Expected: "proxied: SimpleServiceImpl"
but: was "SimpleServiceImpl"
at org.hamcrest.MatcherAssert.assertThat(MatcherAssert.java:20)
at org.junit.Assert.assertThat(Assert.java:956)
at org.junit.Assert.assertThat(Assert.java:923)
at com.neriudon.example.ProxySampleApplicationTests.proxiedToStringTest(ProxySampleApplicationTests.java:27)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.springframework.test.context.junit4.statements.RunBeforeTestExecutionCallbacks.evaluate(RunBeforeTestExecutionCallbacks.java:73)
at org.springframework.test.context.junit4.statements.RunAfterTestExecutionCallbacks.evaluate(RunAfterTestExecutionCallbacks.java:83)
at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:75)
at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86)
at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:251)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:97)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:190)
at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:86)
at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:538)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:760)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:460)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:206)
う~ん
ちなみにservice = adviser.apply(service);
にブレイクポイントをかけると、JdkDynamicAopProxy
でプロキシ化されていることが確認できました。
JdkDynamicAopProxyはダメ
ここでtoString()
について考えてみると、メソッドの定義はObject
クラスで定義されているわけですが……そのObject
クラスは何もインタフェースを実装していません。
なのでインタフェースベースのJdkDynamicAopProxy
でプロキシ化した場合、toString()
に処理を割り込めないんですね。
CGLIBでプロキシ化して解決
というわけで、実体クラスでプロキシ化するためにCGLIBでプロキシ化します。
public ToStringAdvisor() {
proxy = new ToStringProxy<>();
// CGLIBでプロキシ化する
proxy.setProxyTargetClass(true);
// 実体クラスのtoString()を対象とする
String expression = "execution(* " + SimpleServiceImpl.class.getName() + ".toString())";
AspectJExpressionPointcutAdvisor pointcutAdvisor = new AspectJExpressionPointcutAdvisor();
pointcutAdvisor.setAdvice(new ToStringInterceptor());
pointcutAdvisor.setExpression(expression);
proxy.addAdvisor(SimpleService.class, pointcutAdvisor);
結果、めでたし、めでたし。
投稿に至った経緯
実際はもっと複雑なインタフェースをプロキシ化してtoString()
を呼び出していたのですが、インタフェースでもtoString()
を呼べてしまうため、なかなか原因に気づきませんでした。
プロキシはJdkDynamicAopProxy
とCGLIB
の2種類しかないのに奥が深いです。
しかし、特定のインタフェースのtoString()
に処理を挟み込みたい場合は、そのインタフェースを実装したすべてのクラスに対してCGLIB
でプロキシ化しないといけないんでしょうか?