2
0

More than 5 years have passed since last update.

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

Posted at

開発環境

  • 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があるとします。

ToStringService.java
public interface SimpleService {

  void foo();

}
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();
  }
}

実行するまでもないですが、SimpleServiceImpltoString()を実行すると"SimpleServiceImpl"が出力されます。

プロキシ化する

ProxyConfigを継承し、任意のクラスをプロキシ化するクラスを作成します。名前は適当です。

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

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()));
    }
  }
}

※サンプルなので入力チェックとか省いています。

SimpleServiceImpltoString()が呼ばれると"proxied: SimpleServiceImpl"と出力される想定です。

テストしてみる

では、さっそくToStringInterceptorに書いた処理が呼ばれるかテストしてみます。

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"));
  }
}

結果は……

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でプロキシ化します。

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()を呼べてしまうため、なかなか原因に気づきませんでした。
プロキシはJdkDynamicAopProxyCGLIBの2種類しかないのに奥が深いです:confounded:

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

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0