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#invoke
和MethodHandle#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性能。