mybatis / mybatis-3

MyBatis SQL mapper framework for Java
http://mybatis.github.io/mybatis-3/
Apache License 2.0
19.8k stars 12.86k forks source link

Interceptor optimization #1993

Open brucelwl opened 4 years ago

brucelwl commented 4 years ago

If there are multiple interceptors intercepting the same method, the same number of dynamic proxy classes will be created. However, the dynamic proxy has certain performance loss. I hope that it can be optimized to avoid the creation of multiple dynamic proxy classes. example:

@Intercepts({
        @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})

})
public class MyInterceptor implements Interceptor {
    private static final Logger logger = LoggerFactory.getLogger(MyInterceptor.class);

    private Properties properties;

    @Override
    public void setProperties(Properties properties) {
        this.properties = properties;
    }

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        logger.info("invoke");
        return invocation.proceed();
    }
}
@Intercepts({
        @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})

})
public class MyInterceptor2 implements Interceptor {

}

This will cause two dynamic proxy classes to be created !!!

At present, Mybatis supports four types of interface interception ParameterHandler, ResultSetHandler, StatementHandler, Executor

Proposal 1: change to responsibility chain mode,developers can provide specific implementation classes of these interfaces to intercept corresponding methods

Proposal 2: Only a proxy class can be used to intercept interface methods,The advantage of this approach is that it does not change the existing interceptor implementation logic

brucelwl commented 4 years ago

@harawata Can you reply to my issue?

harawata commented 4 years ago

Hello @brucelwl ,

I'm sorry, but I am not so familiar with this topic. Could you post some data showing the 'certain performance loss', please?

brucelwl commented 4 years ago

@harawata I did a test. If an interface is proxy only once, the performance is much higher than that of proxy multiple times, and the performance increases in proportion to the number of agents

environment: windows 10 JDK:JDK 1.8.0_171, Java HotSpot(TM) 64-Bit Server VM, 25.171-b11 CPU: i5 pc memory:16G Other: all defaults Benchmarking tools:JMH

Benchmark code

public interface ExecutorHandler {
    int getProxyCount();

    int update(MappedStatement ms, Object parameter) throws SQLException;

    <E> List<E> query(MappedStatement ms, Object parameter);
}

public class ExecutorHandlerImpl implements ExecutorHandler {

    private int count;

    public ExecutorHandlerImpl(int count) {
        this.count = count;
    }

    @Override
    public int getProxyCount() {
        return count;
    }

    @Override
    public int update(MappedStatement ms, Object parameter) throws SQLException {
        return 10 * 20 / 5 * 2 + 1 + 15;
    }

    @Override
    public <E> List<E> query(MappedStatement ms, Object parameter) {
        return null;
    }
}
public class Plugin implements InvocationHandler {

    private final Object target;

    private Plugin(Object target) {
        this.target = target;
    }

    public static Object wrap(Object target) {
        Class<?> type = target.getClass();
        Class<?>[] interfaces = getAllInterfaces(type);
        if (interfaces.length > 0) {
            return Proxy.newProxyInstance(type.getClassLoader(), interfaces, new Plugin(target));
        }
        return target;
    }

    private static Class<?>[] getAllInterfaces(Class<?> type) {
        Set<Class<?>> interfaces = new HashSet<>();
        while (type != null) {
            interfaces.addAll(Arrays.asList(type.getInterfaces()));
            type = type.getSuperclass();
        }
        return interfaces.toArray(new Class<?>[0]);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        return method.invoke(target, args);
    }
}

public class InterceptorChain {

    public Object pluginAll(ExecutorHandler target) {
        int proxyCount = target.getProxyCount();

        for (int i = 0; i < proxyCount; i++) {
            target = (ExecutorHandler) Plugin.wrap(target);
        }
        return target;
    }
}
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations = 1) 
@Threads(15)
@State(Scope.Benchmark)
@Measurement(iterations = 2, time = 5, timeUnit = TimeUnit.SECONDS)
@OutputTimeUnit(TimeUnit.NANOSECONDS) 
public class ProxyBenchmarkTest {
    private static final Logger logger = LoggerFactory.getLogger(ProxyBenchmarkTest.class);

    ExecutorHandler executorHandler;
    ExecutorHandler executorHandler2;

    @Setup
    public void setup() throws Exception {
        InterceptorChain interceptorChain = new InterceptorChain();
        executorHandler = (ExecutorHandler) interceptorChain.pluginAll(new ExecutorHandlerImpl(1));
        executorHandler2 = (ExecutorHandler) interceptorChain.pluginAll(new ExecutorHandlerImpl(15));
    }

    @Benchmark
    public void jdkProxyN(Blackhole blackhole) throws Exception {
        MappedStatement mappedStatement = new MappedStatement(
                "select * from user info", 5,500,"c://aaa.mapper");

        List<Object> query = executorHandler2.query(mappedStatement, 45);
        blackhole.consume(query);
    }

    @Benchmark
    public void jdkProxy1(Blackhole blackhole) throws Exception {
        MappedStatement mappedStatement = new MappedStatement(
                "select * from user info", 5,500,"c://aaa.mapper");

        List<Object> query = executorHandler.query(mappedStatement, 45);
        blackhole.consume(query);
    }

    public static void main(String[] args) throws Exception {
        Options options = new OptionsBuilder().include(ProxyBenchmarkTest.class.getName())
                //.output("benchmark/jedis-Throughput.log")
                .forks(0)
                .build();
        new Runner(options).run();

    }
}

Proxy 1 time vs Proxy 3 times image

Proxy 1 time vs Proxy 5 times image

Proxy 1 time vs Proxy 8 times image

brucelwl commented 4 years ago

@harawata see PR https://github.com/mybatis/mybatis-3/pull/2001/files please This PR has two main purposes:

  1. The first goal is performance improvement
  2. The second goal is to make the generated proxy class structure clearer

The current implementation code will make the proxy class nest another proxy. The more interceptors, the more nested the proxy class. But in fact, if only one proxy class is used, the class structure will be clearer

harawata commented 4 years ago

Thank you, @brucelwl .

I will look into it, but it may take some time as I don't have much spare time lately.

brucelwl commented 4 years ago

@harawata Another more optimized way is to use the decorator pattern to form an interceptor chain, so that there is no need to use a dynamic proxy to implement the interceptor, If you agree, I can submit a pr

harawata commented 4 years ago

@brucelwl ,

Just curious, how many plugins do you use in your project? I don't use plugins very often and have never had any performance issue myself.

Anyway, I need some time to understand the current design before answering your question. It would be easier for us to understand your proposals if you submit the change as a PR indeed, but please do understand there is no guarantee that your PRs will be merged.

brucelwl commented 4 years ago

@harawata The performance problem is only relative. I just want to have an optimized solution

wushp commented 4 years ago

@harawata ありがとう!