架构(十六)本地方法缓存

一、引言

作者需要在底层公共包里面加一个方法反射的工具类,看起来很简单的事,问题也不少,这里讲讲过程。在结合同事的思维误区聊聊本地加锁块的问题。

二、方案选型

其实一开始有两种方案,一种是传入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);
 }
}

五、总结

看起来很简单的东西做起来其实有很多细节要考虑,很多技术细节大家也都不是那么确定的,千万不要在自己有疑惑的时候听别人的技术理念,有疑惑就要自己去尝试验证。

相关推荐
qq_58956810几秒前
封装工具类,JwtUtils令牌工具类
java
400分21 分钟前
git踩坑之修改仓库首次提交(根提交)的commit信息
架构
漫随流水24 分钟前
创建一个IDEA的Java项目
java·ide·intellij-idea
Hammer_Hans24 分钟前
DFT笔记45
java·jvm·笔记
倔强的石头10627 分钟前
告别昂贵的ETL——大数据架构下的时序选型指南
大数据·架构·etl
ABILI .30 分钟前
主动类型转换
java
奋斗的老史31 分钟前
LangChain4j 进阶实战系列
java·langchain4j·ai应用开发
橙子圆12335 分钟前
Redis知识2
java·数据库·redis
callJJ37 分钟前
Codex 联动 OpenSpec 提效方法论
java·开发语言·codex·openspec
非情剑37 分钟前
Tlog实现微服务日志追踪
微服务·云原生·架构