一、引言
作者需要在底层公共包里面加一个方法反射的工具类,看起来很简单的事,问题也不少,这里讲讲过程。在结合同事的思维误区聊聊本地加锁块的问题。
二、方案选型
其实一开始有两种方案,一种是传入Function和入参,一种是传入实例、方法名、入参
两种都可以,但是一开始想着尽量让使用方少操作,而且反射有性能损耗,所以还是先研究了传入Function和入参
1、传入Function
java
public static <T, R> R invoke(Function<T, R> f, T request) {
return f.apply(request);
}
这样就很简单,用的时候还是从spring里面拿出来
java
@Resource
private ExecuteProcess executeProcess;
executeProcess::execute.request
这其实是可以的,但是对于作者的需求来说不行,因为作者需要通过传入类的实例拿到他注解上的一些属性,但是通过Function是拿不到的
2、反射
反射需要传入类的实例,参数和方法名,也就多了一个参数
这里要说一下request就可以了,随着目前的规范化代码,重载方法在业务系统里面基本是会被骂的,大家都是放一个对象,入参有增加了,对象加一个参数就可以,而不是把重载方法都搞一遍
java
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
java
/**
* 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);
}
}
}
四、拓展
在这个过程中,同事和作者产生过一个技术的分歧,他觉得锁本地静态变量methodCache会导致整个工具类被锁,其他线程会卡在invoke(T instance, T request, String methodName)外面进不来
作者认为他锁的是局部代码块,也就是invoke进得去,但是其他线程会卡在if (meMap == null) {,有争论就做个实验好了,虽然说也没有准备锁methodCache,但是技术理念不能错,不然一定会在别的地方踩坑
作者在加锁的方法做了延迟,然后开了两个线程,第一个抢到的线程会等第二个线程进来,如果第二个线程的日志被挡在invoke外面就是同事说的对,不然就是作者对的
事实证明作者是对的,这里作者也不把截图贴上去了,有兴趣的可以自己试试,多动手,少动嘴
java
/**
* 缓存类实例、方法名对应的方法
*/
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);
}
}
五、总结
看起来很简单的东西做起来其实有很多细节要考虑,很多技术细节大家也都不是那么确定的,千万不要在自己有疑惑的时候听别人的技术理念,有疑惑就要自己去尝试验证。