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

一、引言

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

二、方案选型

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

五、总结

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

相关推荐
paopaokaka_luck几秒前
[371]基于springboot的高校实习管理系统
java·spring boot·后端
以后不吃煲仔饭13 分钟前
Java基础夯实——2.7 线程上下文切换
java·开发语言
进阶的架构师14 分钟前
2024年Java面试题及答案整理(1000+面试题附答案解析)
java·开发语言
The_Ticker19 分钟前
CFD平台如何接入实时行情源
java·大数据·数据库·人工智能·算法·区块链·软件工程
大数据编程之光41 分钟前
Flink Standalone集群模式安装部署全攻略
java·大数据·开发语言·面试·flink
爪哇学长1 小时前
双指针算法详解:原理、应用场景及代码示例
java·数据结构·算法
ExiFengs1 小时前
实际项目Java1.8流处理, Optional常见用法
java·开发语言·spring
paj1234567891 小时前
JDK1.8新增特性
java·开发语言
繁依Fanyi1 小时前
简易安卓句分器实现
java·服务器·开发语言·算法·eclipse