一、引言
作者需要在底层公共包里面加一个方法反射的工具类,看起来很简单的事,问题也不少,这里讲讲过程。在结合同事的思维误区聊聊本地加锁块的问题。
二、方案选型
其实一开始有两种方案,一种是传入Function和入参,一种是传入实例、方法名、入参
两种都可以,但是一开始想着尽量让使用方少操作,而且反射有性能损耗,所以还是先研究了传入Function和入参
1、传入Function
public static <T, R> R invoke(Function<T, R> f, T request) {
return f.apply(request);
}
这样就很简单,用的时候还是从spring里面拿出来
@Resource
private ExecuteProcess executeProcess;
executeProcess::execute.request
这其实是可以的,但是对于作者的需求来说不行,因为作者需要通过传入类的实例拿到他注解上的一些属性,但是通过Function是拿不到的
2、反射
反射需要传入类的实例,参数和方法名,也就多了一个参数
这里要说一下request就可以了,随着目前的规范化代码,重载方法在业务系统里面基本是会被骂的,大家都是放一个对象,入参有增加了,对象加一个参数就可以,而不是把重载方法都搞一遍
public static <T, R> R invoke(T instance, T request, String methodName) {
try {
Class<?> clazz = instance.getClass();
Method method = clazz.getMethod(methodName, request.getClass());
// 执行方法
R result = (R)method.invoke(instance, request);
return result;
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
// 异常处理
throw new SoaInvokeException(e);
}
}
三、优化
选型之后就要对这个实现进行优化了,那么反射可以在哪里进行优化呢,method.invoke肯定是少不了的,但是执行方法可以被缓存,而不是每次都要反射遍历那就先看看chatGpt有没有什么建议
1、chatGpt建议
他先是让用方法名字作为key进行缓存,这有问题,但是不同的类之间没有同名方法吗
然后再问了一下,又说用pair.of把类和方法作为有序对,但是这也有问题,这相当于每次调用这个方法都在创建pair对象
2、自优化
上面chatGpt的两种方法不予采纳,那就自己进行优化吧
这里其实有一个疑问点,那就是第一次加锁的时候,到底是锁methodCache还是clazz呢,锁methodCache有点大,锁clazz又怕别人也有在锁的,这时候就要根据实际情况了
我们使用的时候基本不会加这个clazz,框架倒是有可能,但是分析了这一类clazz,至少我们传入的应该是不会被锁的,所以最终选了clazz
/**
* map的大小
*/
private static final int INIT_SIZE = 16;
/**
* 缓存类实例、方法名对应的方法
*/
protected static ConcurrentHashMap<Class<?>, ConcurrentHashMap<String, Method>> methodCache =
new ConcurrentHashMap<>(INIT_SIZE);
/**
* 获取执行方法
*
* @param instance 类
* @param request 请求参数
* @param methodName 接口名称
* @param <T>
* @return
* @throws NoSuchMethodException
*/
private static <T> Method getMethod(T instance, T request, String methodName) throws NoSuchMethodException {
Class<?> clazz = instance.getClass();
Method method;
ConcurrentHashMap<String, Method> meMap = methodCache.get(clazz);
String meKey = methodName + request.getClass().getName();
if (meMap == null) {
synchronized (clazz) {
meMap = methodCache.get(clazz);
if (meMap == null) {
method = clazz.getMethod(methodName, request.getClass());
meMap = new ConcurrentHashMap(INIT_SIZE);
meMap.put(meKey, method);
// 将方法对象存入缓存
methodCache.put(clazz, meMap);
return method;
}
}
}
method = meMap.get(meKey);
if (method == null) {
synchronized (meMap) {
method = meMap.get(meKey);
if (method == null) {
method = clazz.getMethod(methodName, request.getClass());
meMap.put(meKey, method);
}
}
}
return method;
}
public static <T, R> R invoke(T instance, T request, String methodName) {
try {
Method method = getMethod(instance, request, methodName);
// 执行方法
R result = (R)method.invoke(instance, request);
return result;
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
// 异常处理
throw new SoaInvokeException(e);
}
}
}
3、review
在大家进行讨论分析的时候,提出了新的看法,主要是加一些校验和使用ConcurrentHashMap提供的api,这样代码更加简洁严谨
/**
* map的大小
*/
private static final int INIT_SIZE = 2;
/**
* 缓存类实例、方法名对应的方法
*/
protected static ConcurrentHashMap<Class<?>, ConcurrentHashMap<String, Method>> methodCache =
new ConcurrentHashMap<>(INIT_SIZE);
/**
* 拼接请求方法
*/
private static final String REQUEST_METHOD = "_";
/**
* 不是静态方法,也不是合成方法,也不是桥接方法 才可以反射调用
*
* @param method
* @return
*/
private static boolean isIncludedMethod(Method method) {
if (Modifier.isStatic(method.getModifiers()) || method.isSynthetic() || method.isBridge()) {
return false;
}
return true;
}
/**
* 检查入参
*
* @param instance
* @param <I>
* @throws IllegalArgumentException
*/
private static <I> void check(I instance) throws IllegalArgumentException {
if (instance == null) {
throw new IllegalArgumentException("obj is null");
}
}
/**
* 反射获取方法
*
* @param clazz
* @param request
* @param methodName
* @param <T>
* @return
* @throws NoSuchMethodException
*/
private static <T> Method collectMethod(Class<?> clazz, T request, String methodName) throws NoSuchMethodException {
Method method = clazz.getMethod(methodName, request.getClass());
if (isIncludedMethod(method)) {
return method;
}
throw new NoSuchMethodException("No such method: " + methodName);
}
/**
* 缓存获取执行方法
*
* @param instance 类
* @param request 请求参数
* @param methodName 接口名称
* @param <T>
* @return
*/
private static <I, T> Method getMethod(I instance, T request, String methodName) {
// 校验
check(instance);
Class<?> clazz = instance.getClass();
String meKey = request.getClass().getName() + REQUEST_METHOD + methodName;
return methodCache.computeIfAbsent(clazz, clz -> new ConcurrentHashMap<>(INIT_SIZE)).computeIfAbsent(meKey,
me -> {
try {
return collectMethod(clazz, request, methodName);
} catch (NoSuchMethodException e) {
throw new SoaInvokeException(e);
}
});
}
/**
*
* @param instance 类的实例
* @param request 请求参数
* @param methodName 接口名称
* @param <I>
* @param <T>
* @param <R>
* @return
*/
public static <I, T, R> R invoke(I instance, T request, String methodName) {
try {
Method method = getMethod(instance, request, methodName);
// 执行方法
R result = (R)method.invoke(instance, request);
return result;
} catch (IllegalAccessException | InvocationTargetException e) {
// 异常处理
throw new SoaInvokeException(e);
}
}
四、拓展
在这个过程中,同事和作者产生过一个技术的分歧,他觉得锁本地静态变量methodCache会导致整个工具类被锁,其他线程会卡在invoke(T instance, T request, String methodName)外面进不来
作者认为他锁的是局部代码块,也就是invoke进得去,但是其他线程会卡在if (meMap == null) {,有争论就做个实验好了,虽然说也没有准备锁methodCache,但是技术理念不能错,不然一定会在别的地方踩坑
作者在加锁的方法做了延迟,然后开了两个线程,第一个抢到的线程会等第二个线程进来,如果第二个线程的日志被挡在invoke外面就是同事说的对,不然就是作者对的
事实证明作者是对的,这里作者也不把截图贴上去了,有兴趣的可以自己试试,多动手,少动嘴
/**
* 缓存类实例、方法名对应的方法
*/
protected static ConcurrentHashMap<Class<?>, ConcurrentHashMap<String, Method>> methodCache =
new ConcurrentHashMap<>();
public static void main(String[] args) {
ServiceServerExtensionUtil util = new ServiceServerExtensionUtil();
ServiceServerExtensionUtilTwo utilTwo = new ServiceServerExtensionUtilTwo();
// 反射方法执行成功, 方法被缓存
Thread a = new Thread(() -> {
System.out.println("thread:" + Thread.currentThread().getName());
ServiceServerExtension.invoke(util, "test", "invokeUtil");
});
Thread b = new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("thread:" + Thread.currentThread().getName());
ServiceServerExtension.invoke(utilTwo, "test", "invokeUtil");
});
a.start();
b.start();
}
/**
* 获取执行方法
*
* @param clazz 类
* @param request 请求参数
* @param methodName 接口名称
* @param <T>
* @return
* @throws NoSuchMethodException
*/
private static <T> Method getMethod(Class<?> clazz, T request, String methodName) throws NoSuchMethodException {
System.out.println("thread:" + Thread.currentThread().getName() + "getMethod start");
Method method;
ConcurrentHashMap<String, Method> meMap = methodCache.get(clazz);
if (meMap == null) {
synchronized (methodCache) {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
meMap = methodCache.get(clazz);
if (meMap == null) {
method = clazz.getMethod(methodName, request.getClass());
meMap = new ConcurrentHashMap(16);
meMap.put(methodName, method);
// 将方法对象存入缓存
methodCache.put(clazz, meMap);
System.out.println("thread:" + Thread.currentThread().getName() + "getMethod end");
return method;
}
}
}
method = meMap.get(methodName);
if (method == null) {
synchronized (meMap) {
method = meMap.get(methodName);
if (method == null) {
method = clazz.getMethod(methodName, request.getClass());
meMap.put(methodName, method);
}
}
}
System.out.println("thread:" + Thread.currentThread().getName() + "getMethod end");
return method;
}
public static <T, R> R invoke(T instance, T request, String methodName) {
try {
Class<?> clazz = instance.getClass();
Method method = getMethod(clazz, request, methodName);
// 执行方法
R result = (R)method.invoke(instance, request);
return result;
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
// 异常处理
throw new SoaInvokeException(e);
}
}
五、总结
看起来很简单的东西做起来其实有很多细节要考虑,很多技术细节大家也都不是那么确定的,千万不要在自己有疑惑的时候听别人的技术理念,有疑惑就要自己去尝试验证。