Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

開発環境

  • 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でプロキシ化しないといけないんでしょうか?

neriudon
大学時代からJavaをやっています。 最近はSpring Framework、主にSpring Integrationにお熱。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした