Mybatis Mapper JDK dynamic proxy MapperProxy implementation, performance analysis optimization

The source code of org.apache.ibatis.binding.MapperProxy.DefaultMethodInvoker in Mybatis is as follows:

private static class DefaultMethodInvoker implements MapperMethodInvoker {
    
    
    private final MethodHandle methodHandle;

    public DefaultMethodInvoker(MethodHandle methodHandle) {
    
    
      super();
      this.methodHandle = methodHandle;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
    
    
      return methodHandle.bindTo(proxy).invokeWithArguments(args);
    }
}

Benchmark test environment

# JMH version: 1.25.2
# Windows 10, 4核 16G

# Warmup: 1 iterations, 2 s each
# Measurement: 2 iterations, 5 s each
# Timeout: 10 min per iteration
# Threads: 15 threads, will synchronize iterations
# Benchmark mode: Average time, time/op

Pressure test code

Mapper interface

public interface UserMapper {
    
    
    default String getUserName(String userName, Boolean yes, int age) {
    
    
        return "bruce " + userName + " " + yes + " " + age;
    }
}

Imitate mybatis MapperProxy to generate a dynamic proxy for the interface

public class UserServiceInvoke {
    
    

    private static final int ALLOWED_MODES = MethodHandles.Lookup.PRIVATE | MethodHandles.Lookup.PROTECTED
            | MethodHandles.Lookup.PACKAGE | MethodHandles.Lookup.PUBLIC;

    public static UserMapper getInstance(InvocationHandler handler) {
    
    
        return (UserMapper) Proxy.newProxyInstance(UserServiceInvoke.class.getClassLoader(),
                new Class[]{
    
    UserMapper.class}, handler);
    }

    /**
     * 仅仅是测试,所以固定调用 getUserName 方法
     */
    public static MethodHandle getMethodHandle(Class<?> declaringClass) {
    
    
        int version = JdkVersion.getVersion();
        if (version < 7) {
    
    
            throw new UnsupportedOperationException("java 7 之前没有java.lang.invoke.MethodHandle");
        }
        MethodHandles.Lookup lookup;
        if (version == 8) {
    
    
            lookup = createJava8HasPrivateAccessLookup(declaringClass);
        } else {
    
    
            lookup = MethodHandles.lookup();
        }
        //通过java.lang.invoke.MethodHandles.Lookup.findSpecial获取子类的父类方法的MethodHandle
        //用于调用某个类的父类方法
        MethodHandle virtual = null;
        try {
    
    
            virtual = lookup.findSpecial(declaringClass, "getUserName",
                    MethodType.methodType(String.class, String.class, Boolean.class, int.class),
                    declaringClass);
        } catch (NoSuchMethodException | IllegalAccessException e) {
    
    
            e.printStackTrace();
        }
        return virtual;
    }

    /**
     * java8 中可能抛出如下异常,需要反射创建MethodHandles.Lookup解决该问题
     * <pre>
     *     java.lang.IllegalAccessException: no private access for invokespecial: interface com.example.demo.methodhandle.UserService, from com.example.demo.methodhandle.UserServiceInvoke
     * 	at java.lang.invoke.MemberName.makeAccessException(MemberName.java:850)
     * 	at java.lang.invoke.MethodHandles$Lookup.checkSpecialCaller(MethodHandles.java:1572)
     * </pre>
     */
    public static MethodHandles.Lookup createJava8HasPrivateAccessLookup(Class<?> declaringClass) {
    
    
        Constructor<MethodHandles.Lookup> lookupConstructor = null;
        try {
    
    
            lookupConstructor = MethodHandles.Lookup.class.getDeclaredConstructor(Class.class, int.class);
            lookupConstructor.setAccessible(true);
            return lookupConstructor.newInstance(declaringClass, ALLOWED_MODES);
        } catch (Exception e) {
    
    
            throw new IllegalStateException("no 'Lookup(Class, int)' method in java.lang.invoke.MethodHandles.", e);
        }
    }
}

JMH benchmark code

/**
 * Created by bruce on 2019/6/23 21:26
 */
@BenchmarkMode(Mode.AverageTime) //测试的模式,可以测试吞吐,延时等;
@Warmup(iterations = 1, time = 2)  //预热的循环次数
@Threads(15)  //开启多少条线程测试
@State(Scope.Benchmark) //@State(Scope.Benchmark) 一个测试启用实例, @State(Scope.Thread) 每个线程启动一个实例
@Measurement(iterations = 2, time = 5, timeUnit = TimeUnit.SECONDS)
//@Measurement(iterations = 2, batchSize = 1024)
@OutputTimeUnit(TimeUnit.NANOSECONDS) //测试结果单位
public class MethodHandleBenchmarkTest {
    
    
    private static final Logger logger = LoggerFactory.getLogger(MethodHandleBenchmarkTest.class);

    UserMapper userMapperInvoke;
    UserMapper userMapperBindToInvoke;

    @Setup
    public void setup() throws Exception {
    
    
        userMapperInvoke = UserServiceInvoke.getInstance(new UserServiceInvoke.MethodHandleInvoke(UserMapper.class));
        userMapperBindToInvoke = UserServiceInvoke.getInstance(new UserServiceInvoke.MethodHandleBindToInvoke(UserMapper.class));
    }

    @Benchmark
    public void bindToInvokeExact(Blackhole blackhole) throws Throwable {
    
    
        String name = userMapperBindToInvoke.getUserName("lwl", true, 15);
        blackhole.consume(name);
    }

    @Benchmark
    public void invoke(Blackhole blackhole) throws Throwable {
    
    
        String o = userMapperInvoke.getUserName("lwl", true, 15);
        blackhole.consume(o);
    }

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

Mainly compare the performance of the invoke method

VM version: JDK 1.8.0_171, Java HotSpot™ 64-Bit Server VM, 25.171-b11

First optimization and existing problems

The first optimization: call the java.lang.invoke.MethodHandle#invokemethod directly , pass in the parameters to execute, the method parameter is a variable parameter, and the first parameter must be the instance object actually called, the source code MethodHandleInvoke. This can avoid calling the methodHandle.bindTo(proxy)method every time and avoid creating a new MethodHandle object every time.

public static class MethodHandleInvoke implements InvocationHandler {
    
    

	MethodHandle methodHandle;

	public MethodHandleInvoke(Class<?> declaringClass) {
    
    
		methodHandle = getMethodHandle(declaringClass);
	}

	@Override
	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    
    
		return methodHandle.invoke(proxy, "lwl", true, 15);
	}
}

The calling method in MethodHandleBindToInvoke is the org.apache.ibatis.binding.MapperProxy.DefaultMethodInvokersame as that in Mybatis , which is mainly used to compare the pressure test results.
invokeWithArgumentsThe method is also a variable parameter.

public static class MethodHandleBindToInvoke implements InvocationHandler {
    
    

	MethodHandle methodHandle;

	public MethodHandleBindToInvoke(Class<?> declaringClass) {
    
    
		methodHandle = getMethodHandle(declaringClass);
	}

	@Override
	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    
    
		return methodHandle.bindTo(proxy).invokeWithArguments("lwl", true, 15);
	}
}

Pressure test results:
Insert picture description here
From the pressure test results, the performance of the calling method in MethodHandleInvoke is indeed much higher, about 6-7 times . After being happy, I found a problem. The parameters in the method are hard-coded. In actual work, it is impossible for us to hard-code the parameters in this place. Should InvocationHandler #invoke(Object proxy, Method method, Object[] args)the Object[] argsincoming methodHandle.invokemethod. Start to modify the code for the second time. . .

Second optimization and existing problems

Because the methodHandle.invoke(Object... args)method parameter is a variable parameter, and the first parameter must be the instance object that is actually called. So you need to create a new Object[] data to store new parameters.

public static class MethodHandleInvoke implements InvocationHandler {
    
    

	MethodHandle methodHandle;

	public MethodHandleInvoke(Class<?> declaringClass) {
    
    
		methodHandle = getMethodHandle(declaringClass);
	}

	@Override
	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    
    
		Object[] objects = new Object[args.length + 1];
		objects[0] = proxy;
		for (int i = 0; i < args.length; i++) {
    
    
			objects[i + 1] = args[i];
		}
		return methodHandle.invoke(objects );
	}
}

MethodHandleBindToInvoke is also the same as DefaultMethodInvoker in Mybatis, but the parameters are no longer fixed and are passed in by methods for performance comparison.

public static class MethodHandleBindToInvoke implements InvocationHandler {
    
    

	MethodHandle methodHandle;

	public MethodHandleBindToInvoke(Class<?> declaringClass) {
    
    
		methodHandle = getMethodHandle(declaringClass);
	}

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

Performing benchmark tests found that the code in MethodHandleBindToInvoke can be executed normally, but the optimized method in MethodHandleInvoke throws the following exception:

java.lang.invoke.WrongMethodTypeException: cannot convert MethodHandle(UserMapper,String,Boolean,int)String to (Object[])Object
	at java.lang.invoke.MethodHandle.asTypeUncached(MethodHandle.java:775)
	at java.lang.invoke.MethodHandle.asType(MethodHandle.java:761)
	at java.lang.invoke.Invokers.checkGenericType(Invokers.java:321)
	at com.example.demo.methodhandle.UserServiceInvoke$MethodHandleInvoke.invoke(UserServiceInvoke.java:86)
	at com.sun.proxy.$Proxy0.getUserName(Unknown Source)
	at com.example.demo.methodhandle.MethodHandleBenchmarkTest.invoke(MethodHandleBenchmarkTest.java:43)

The rough meaning is that the parameter types do not match.
The strange thing is UserMapper#getUserNamemethod to the parameters and the original is exactly the same, and java.lang.invoke.MethodHandle#invokeWithArguments(java.lang.Object...)and java.lang.invoke.MethodHandle#invoke(Object... args)two types of method arguments, too, are variable parameter type. How can one execute normally and the other throw an exception?
Compare the two methods to find the difference between the two.

public final native @PolymorphicSignature Object invoke(Object... args) throws Throwable;

public Object invokeWithArguments(Object... arguments) throws Throwable {
    
    
    MethodType invocationType = MethodType.genericMethodType(arguments == null ? 0 : arguments.length);
    return invocationType.invokers().spreadInvoker(0).invokeExact(asType(invocationType), arguments);
}

The reason is that it MethodHandle#invoke(Object... args)is a nativemethod that throws an exception . This is related to the underlying implementation of the JDK.
See how you cannot directly call MethodHandle#invoke(Object... args) when you are not sure of the number and type of parameters. Can only be changed to the MethodHandle#invokeWithArguments(java.lang.Object...)method of use . Next look at the third optimization.

Third optimization

Use the MethodHandle#invokeWithArguments(java.lang.Object...)method directly , but you still need to create an Object[] array every time, which will be used proxyas the first parameter to execute correctly.

public static class MethodHandleInvoke implements InvocationHandler {
    
    

    MethodHandle methodHandle;

    public MethodHandleInvoke(Class<?> declaringClass) {
    
    
        methodHandle = getMethodHandle(declaringClass);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    
    
        Object[] objects = new Object[args.length + 1];
        objects[0] = proxy;
        for (int i = 0; i < args.length; i++) {
    
    
            objects[i + 1] = args[i];
        }
        return methodHandle.invokeWithArguments(objects);
    }
}

This time the program is executed normally, and the results of the stress test are as follows:
Insert picture description here
This time the parameters are no longer fixed, but the parameters passed in using the method. And from the pressure test results, the optimized usage is indeed higher than the MethodHandleBindToInvokemedium, but this time it is about 3-4 times . The main reason is due MethodHandle#invokeand MethodHandle#invokeWithArgumentsto achieve between different.
But in the case of uncertain parameter classes, we can only use MethodHandle#invokeWithArgumentsmethods.
But in my opinion with code cleanliness, this optimization scheme still seems to be not the most perfect. Every time a new Object[] array is created, this will cause a small slice of jvm heap memory to be opened for each execution. In the memory space, you also need to assign a value to each array element. (Although it may be small, it is theoretically a memory loss. If Young gc increases, it will waste CPU resources.) Next, let's look at the fourth optimization.

Fourth optimization

In fact, it MethodHandleis an immutable object, and DefaultMethodInvokerit can be seen in mybatis that it MethodHandleis saved as a member object.
In the JDK java.lang.reflect.InvocationHandler#invoke(Object proxy, Method method, Object[] args), the proxy passed in each time is the same object, that is, the dynamic proxy class object.
In fact, it is MethodHandle methodHandle = MethodHandle#bindToalso possible to save it. This time MethodHandleBindToInvokethe code in is optimized again.
Use double check lock, if it has been executed, methodHandle.bindTo(proxy)there is no need to show it again.

public static class MethodHandleBindToInvoke implements InvocationHandler {
    
    

    MethodHandle methodHandle;

    private volatile boolean bindTo;

    public MethodHandleBindToInvoke(Class<?> declaringClass) {
    
    
        methodHandle = getMethodHandle(declaringClass);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    
    
        if (!bindTo) {
    
    
            synchronized (this) {
    
    
                if (!bindTo) {
    
    
                    methodHandle = this.methodHandle.bindTo(proxy);
                    bindTo = true;
                }
            }
        }
        return methodHandle.invokeWithArguments(args);
    }
}

Insert picture description here
Judging from the results of the pressure test, the performance is slightly optimized this time. Let's compare it with the usage in Mybatis.
Insert picture description here
It is DefaultMethodInvokerabout 3 times faster than the performance in Mybatis .

Using jdk11 pressure test results:
VM version: JDK 11, Java HotSpot™ 64-Bit Server VM, 11+28 is
Insert picture description here
also DefaultMethodInvokerabout 3-4 times better than the performance in Mybatis .

Expand knowledge

MethodHandles.Lookup#findSpecial

MethodHandles is a class in java7 that is used to obtain method handles corresponding to methods. The function is similar to reflection, but it can have faster calling performance than reflection. java.lang.invoke.MethodHandles.Lookup#findSpecialThe method is mainly used to obtain the method handle of the parent class of the subclass.
In java8, if you call java.lang.invoke.MethodHandles.Lookup#findSpecial to obtain the parent method MethodHandle in a non-subclass method, the following exceptions may be thrown:

java.lang.IllegalAccessException: no private access for invokespecial: interface com.example.demo.methodhandle.UserService, from com.example.demo.methodhandle.UserServiceInvoke
	at java.lang.invoke.MemberName.makeAccessException(MemberName.java:850)
	at java.lang.invoke.MethodHandles$Lookup.checkSpecialCaller(MethodHandles.java:1572)
	at java.lang.invoke.MethodHandles$Lookup.findSpecial(MethodHandles.java:1002)
	at com.example.demo.methodhandle.UserServiceInvoke.getMethodHandle(UserServiceInvoke.java:40)
	at com.example.demo.methodhandle.UserServiceInvoke$MethodHandleBindToInvoke.<init>(UserServiceInvoke.java:75)
	at com.example.demo.methodhandle.MethodHandleBenchmarkTest.setup(MethodHandleBenchmarkTest.java:32)
	at com.example.demo.methodhandle.jmh_generated.MethodHandleBenchmarkTest_bindToInvokeExact_jmhTest._jmh_tryInit_f_methodhandlebenchmarktest0_G(MethodHandleBenchmarkTest_bindToInvokeExact_jmhTest.java:434)
	at com.example.demo.methodhandle.jmh_generated.MethodHandleBenchmarkTest_bindToInvokeExact_jmhTest.bindToInvokeExact_AverageTime(MethodHandleBenchmarkTest_bindToInvokeExact_jmhTest.java:161)

You can refer to the solution in Mybatis and solve it through reflection

public static MethodHandles.Lookup createJava8HasPrivateAccessLookup(Class<?> declaringClass) {
    Constructor<MethodHandles.Lookup> lookupConstructor = null;
    try {
        lookupConstructor = MethodHandles.Lookup.class.getDeclaredConstructor(Class.class, int.class);
        lookupConstructor.setAccessible(true);
        return lookupConstructor.newInstance(declaringClass, ALLOWED_MODES);
    } catch (Exception e) {
        throw new IllegalStateException("no 'Lookup(Class, int)' method in java.lang.invoke.MethodHandles.", e);
    }
}

to sum up

The DefaultMethodInvoker class in MapperProxy can modify the code to optimize the four ways to cache the methodHandle.bindToreturned methodHandleobjects to improve the performance of Mybatis.

Guess you like

Origin blog.csdn.net/u013202238/article/details/108682854