JdkDynamicAopProxyでプロキシ化したInterfaceのtoString()でハマったお話

### 開発環境
- 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`があるとします。

```java:ToStringService.java
public interface SimpleService {

  void foo();

}
```
```java:ToStringServiceImpl.java
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`を継承し、任意のクラスをプロキシ化するクラスを作成します。名前は適当です。

```java:ToStringProxy.java
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`をプロキシ化するアドバイザクラスを作成します。

```java:ToStringAdvisor.java
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`に書いた処理が呼ばれるかテストしてみます。

```java:ProxySampleApplicationTests.java
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"));
  }
}
```

結果は……

```txt:errorlog.txt
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)
```
う～ん:thinking:
ちなみに`service = adviser.apply(service);`にブレイクポイントをかけると、`JdkDynamicAopProxy`でプロキシ化されていることが確認できました。

### JdkDynamicAopProxyはダメ
ここで`toString()`について考えてみると、メソッドの定義は`Object`クラスで定義されているわけですが……その`Object`クラスは何もインタフェースを実装していません。
なのでインタフェースベースの`JdkDynamicAopProxy`でプロキシ化した場合、`toString()`に処理を割り込めないんですね。

### CGLIBでプロキシ化して解決
というわけで、実体クラスでプロキシ化するためにCGLIBでプロキシ化します。

```java:ToStringAdvisor.java
  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種類しかないのに奥が深いです:confounded:。

しかし、特定のインタフェースの`toString()`に処理を挟み込みたい場合は、そのインタフェースを実装したすべてのクラスに対して`CGLIB`でプロキシ化しないといけないんでしょうか？