Mybatis Mapper JDK动态代理MapperProxy实现,性能分析优化

Mybatis中的org.apache.ibatis.binding.MapperProxy.DefaultMethodInvoker源码如下:

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

基准测试环境

# 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

压测代码

Mapper接口

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

模仿mybatis MapperProxy,生成接口的动态代理

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 基准测试代码

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

主要对比invoke方法的性能

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

第一次优化及存在问题

第一次优化:直接调用java.lang.invoke.MethodHandle#invoke方法, 传入参数来执行,方法参数是一个可变参数,并且第一个参数必须是实际被调用的实例对象,源码MethodHandleInvoke。这样可以避免每次都调用methodHandle.bindTo(proxy)方法,避免每次都创建一个新的MethodHandle对象。

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

而MethodHandleBindToInvoke中的调用地方式和Mybatis中的org.apache.ibatis.binding.MapperProxy.DefaultMethodInvoker一致,主要用于对比压测结果。
invokeWithArguments方法同样是一个可变参数.

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

压测结果:
在这里插入图片描述
从压测结果看,MethodHandleInvoke中的调用方式性能确实高了很多,大约6-7倍左右。开心之后,却发现一个问题,方法中的参数是写死的,实际工作中,我们不可能在这个地方写死参数。应该将InvocationHandler #invoke(Object proxy, Method method, Object[] args)中的Object[] args传入methodHandle.invoke方法。开始第二次修改代码。。。

第二次优化及存在问题

由于methodHandle.invoke(Object... args)方法参数是一个可变参数,并且第一个参数必须是实际被调用的实例对象。所以需要创建一个新的Object[]数据,存放新的参数。

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 同样和Mybatis中的DefaultMethodInvoker一致,但是参数不再固定,由方法传入,用做性能对比。

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

执行基准测试却发现.MethodHandleBindToInvoke中的代码可以正常执行,但是MethodHandleInvoke中优化的方式却抛出了如下异常:

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)

大致意思就是参数类型不匹配。
奇怪的是UserMapper#getUserName方法中给参数和原来的是完全一样的,而且java.lang.invoke.MethodHandle#invokeWithArguments(java.lang.Object...)java.lang.invoke.MethodHandle#invoke(Object... args)两个方法参数类型也是一样,都是可变参数类型。怎么会一个正常执行,一个抛出异常呢?
对比了两个方法发现两者的区别.

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

原因在于抛出异常的MethodHandle#invoke(Object... args)是一个native方法. 这就和JDK底层实现有关系了。
看样在不确定参数个数和类型的情况下,是不能直接调用MethodHandle#invoke(Object… args)方法。只能改成使用MethodHandle#invokeWithArguments(java.lang.Object...)方法了。接下来看第三次优化。

第三次优化

直接改用MethodHandle#invokeWithArguments(java.lang.Object...)方法,但还是每次都需要创建一个Object[]数组,将proxy做为第一参数,才能正确执行。

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

这次程序正常执行,压测结果如下:
在这里插入图片描述
这次参数不再是固定写死,而是使用方法传入的参数。并且从压测结果看,优化的使用方式确实比MethodHandleBindToInvoke中的高,但是这次在3-4倍左右。主要原因还是由于MethodHandle#invokeMethodHandle#invokeWithArguments之间的实现不同。
但是在不确定参数类这种场景下,我们只能使用MethodHandle#invokeWithArguments方法。
但是在有代码洁癖的我看来,这次优化方案似乎仍然不是最完美的,每次都去创建一个新的Object[]数组,这就会导致每次执行都在jvm堆内存上开辟一小片内存空间,还需要给每一个数组元素赋值。(虽然可能很小,但理论上是一种内存损耗,如果Young gc变多还会浪费CPU资源),接下来看第四次优化。

第四次优化

实际上MethodHandle是一个不可变对象,在mybatis的DefaultMethodInvoker中可以看到MethodHandle是做为成员对象被保存。
在JDK中java.lang.reflect.InvocationHandler#invoke(Object proxy, Method method, Object[] args),每次传入的proxy都是同一个对象,即动态代理类对象。
其实只要保存MethodHandle methodHandle = MethodHandle#bindTo同样是可以的。这次把MethodHandleBindToInvoke中的代码再做一次优化。
使用双重校验锁,如果已经执行过methodHandle.bindTo(proxy)则不必再示行。

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

在这里插入图片描述
从压测结果看,这次性能还有略有优化。下面再和Mybatis中的使用方式做对比。
在这里插入图片描述
比Mybatis中的DefaultMethodInvoker性能提升约3倍.

使用jdk11压测结果:
VM version: JDK 11, Java HotSpot™ 64-Bit Server VM, 11+28
在这里插入图片描述
同样比Mybatis中的DefaultMethodInvoker性能提升约3-4倍.

扩展知识

MethodHandles.Lookup#findSpecial

MethodHandles是java7中的类,用于获取方法对应的方法句柄,功能类似于反射,但是可以拥有比反射更快的调用性能。java.lang.invoke.MethodHandles.Lookup#findSpecial方法,主要用于获取子类的父类的方法句柄。
java8中如果非子类方法中调用java.lang.invoke.MethodHandles.Lookup#findSpecial获取父类方法MethodHandle,可能会抛出如下异常:

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)

可以参考Mybatis中的解决方案,通过反射来解决

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

总结

MapperProxy 中的DefaultMethodInvoker类,可以将代码修改成优化四中方式,缓存methodHandle.bindTo返回的methodHandle对象,以此来提升Mybatis性能。

猜你喜欢

转载自blog.csdn.net/u013202238/article/details/108682854