Dubbo源码篇02---从泛化调用探究Wrapper机制的原理

news2024/9/27 17:27:19

Dubbo源码篇02---从泛化调用探究Wrapper机制的原理

  • 什么是泛化调用
    • 从传统三层架构说起
      • 反射调用尝试优化
    • 泛化调用
      • 泛化调用怎么用
        • 通过Spring使用泛化调用
      • 利用泛化调用改造现有服务
    • 泛化调用小结
  • Wrapper机制
    • 自定义代理
    • dubbo底层wrapper原理
    • 小结
  • 小结


什么是泛化调用

从传统三层架构说起

对于传统的三层架构而言,Controller层负责接收请求,Service层负责处理与业务逻辑相关的请求,Dao层负责与数据库进行交互,配合Model模型对象承载业务数据,在请求上下文中传递,最终处理填充完毕数据后,交由View视图进行渲染:

在这里插入图片描述
但是在微服务场景下,Service层中难免会涉及到RPC远程调用请求,因此上面的流程图就变成了下面这样子:
在这里插入图片描述

上面是rpc远程调用的一种情况,还有一种情况如下所示:
在这里插入图片描述
在这种情况下,我们的Controller层代码可能如下所示:

@RestController
public class UserController {
    // 响应码为成功时的值
    public static final String SUCC = "000000";
    
    // 定义访问下游查询用户服务的字段
    @DubboReference
    private UserQueryFacade userQueryFacade;
    
    // 定义URL地址
    @PostMapping("/queryUserInfo")
    public String queryUserInfo(@RequestBody QueryUserInfoReq req){
        // 将入参的req转为下游方法的入参对象,并发起远程调用
        QueryUserInfoResp resp = 
                userQueryFacade.queryUserInfo(convertReq(req));
        
        // 判断响应对象的响应码,不是成功的话,则组装失败响应
        if(!SUCC.equals(resp.getRespCode())){
            return RespUtils.fail(resp);
        }
        
        // 如果响应码为成功的话,则组装成功响应
        return RespUtils.ok(resp);
    }
}

在这种情况下,我们的Web服务器只是编写代码把数据做了一下包装,然后给到下游系统,等收到下游系统返回的内容后,啥也不做直接返回给前端,此时Web服务器本质是在做一些透传性质的事情。

这里我们只写了一个接口,但是如果有很多接口的Controller层逻辑都是简单的透传,那么能不能把这个逻辑抽取成通用逻辑呢?


反射调用尝试优化

我们先尝试使用反射将透传逻辑的公共部分抽取出来:

  • 传入要调用的service服务接口,及要调用的服务接口名,然后通过反射获取对应的Method对象
  • 将请求参数序列化为JSON字符串,再通过反序列化,将JSON字符串转换为下游接口的入参对象
  • 通过method.invoke反射发起真正的远程调用,并拿到响应对象
  • 通过Ognl表达式语言从响应对象中取出respCode响应码做判断,并最终返回
@RestController
public class UserController {
    // 响应码为成功时的值
    public static final String SUCC = "000000";
    
    // 定义访问下游查询用户服务的字段
    @DubboReference
    private UserQueryFacade userQueryFacade;
    
    // 定义URL地址
    @PostMapping("/queryUserInfo")
    public String queryUserInfo(@RequestBody QueryUserInfoReq req){
        // 调用公共方法
        return commonInvoke(userQueryFacade, "queryUserInfo", req);
    }
    
    /**
     * 模拟公共的远程调用方法
     * @param reqObj:下游的接口的实例对象,即通过 @DubboReference 得到的对象。
     * @param mtdName:下游接口的方法名。
     * @param reqParams:需要请求到下游的数据。
     * @return 直接结果数据。
     */
    public static String commonInvoke(Object reqObj, String mtdName, Object reqParams) throws InvocationTargetException, IllegalAccessException {
        // 通过反射找到 reqObj(例:userQueryFacade) 中的 mtdName(例:queryUserInfo) 方法
        Method reqMethod = ReflectionUtils.findMethod(reqObj.getClass(), mtdName);
        // 并设置查找出来的方法可被访问
        ReflectionUtils.makeAccessible(reqMethod);
        
        // 通过序列化工具将 reqParams 序列化为字符串格式
        String reqParamsStr = JSON.toJSONString(reqParams);
        // 然后再将 reqParamsStr 反序列化为下游对象格式,并反射调用 invoke 方法
        Object resp =  reqMethod.invoke(reqObj, JSON.parseObject(reqParamsStr, reqMethod.getParameterTypes()[0]));
        
        // 判断响应对象的响应码,不是成功的话,则组装失败响应
        if(resp == null || !SUCC.equals(OgnlUtils.getValue(resp, "respCode"))){
            return RespUtils.fail(resp);
        }
        // 如果响应码为成功的话,则组装成功响应
        return RespUtils.ok(resp);
    }
}
  • OGNL(Object-Graph Navigation Language)是一种基于表达式语言的Java对象图导航工具,可以用于简化Java应用程序中的对象导航和操作。OGNL提供了一组简单而强大的语法和运算符,可以轻松地在Java对象图中导航,访问对象属性、调用方法等。

  • OGNL最初是为Struts框架开发的,用于在JSP页面中直接访问Java对象。但是,由于其灵活性和强大性,OGNL已经成为了许多Java框架和应用程序中常用的工具,如Spring框架、Hibernate
    ORM框架等。

  • 下面是一些常见的OGNL表达式示例:

    • 访问对象属性:user.name
    • 调用对象方法:user.getName()
    • 操作集合元素:users.{? #this.age >18}.name
    • 操作数组元素:items[0].name
    • 使用逻辑运算符:user.age > 18 && user.gender == “male”
    • 使用条件运算符:user.age > 18 ? “adult” : “minor”
  • OGNL的优点包括:

    • 语法简单:OGNL使用类似于Java的表达式语言,易于理解和使用。
    • 导航方便:OGNL可以轻松地在Java对象图中导航,访问对象属性、调用方法等。
    • 功能强大:OGNL提供了一组简单而强大的语法和运算符,可以完成许多复杂的对象操作。
    • 可扩展性:OGNL可以轻松地扩展和自定义,以满足不同的应用程序需求。
  • 总的来说,OGNL是一个功能强大、易于使用的Java对象导航工具,可以使Java应用程序的开发更加简单和高效。

虽然我们通过反射抽取了远程方法调用流程的公共逻辑,使得单个controller接口内部的逻辑精简了许多,但是我们仍然需要定义很多类似于queryUserInfo这样的请求接口,那么有没有办法以一个统一的请求接口作为入口地址,统一处理所有透传式接口请求呢?


泛化调用

要以一个统一的请求接口作为入口地址,其实就类似于DispatchServlet统一拦截处理所有servlet请求的思路一样,然后再由DispatcherServlet按照路由规则派发给各个控制器进行请求处理:
在这里插入图片描述
我们这里的思路,就是编写一个统一的次级控制处理器,拦截所有请求,按照上面封装好的通用逻辑,发起RPC请求调用,然后返回远程调用结果即可。

上面的代码改造思路如下:

  • 定义一个公共的次级控制处理器CommonController
  • 定义统一的URL路径/gateway/{className}/{mtdName}/request ,将className和mtdName做成请求路径的占位符
  • 修改请求业务参数格式定义,由对象转换String
  • 在原有的CommonInvoke逻辑中,利用类加载器加载ClassName对应的服务调用接口,然后想办法找到ClassName对应的实例对象
@RestController
public class CommonController {
    // 响应码为成功时的值
    public static final String SUCC = "000000";
    
    // 定义URL地址
    @PostMapping("/gateway/{className}/{mtdName}/request")
    public String commonRequest(@PathVariable String className,
                                @PathVariable String mtdName,
                                @RequestBody String reqBody){
        // 将入参的req转为下游方法的入参对象,并发起远程调用
        return commonInvoke(className, mtdName, reqBody);
    }
    
    /**
     * 模拟公共的远程调用方法
     * @param className:下游的接口归属方法的全类名。
     * @param mtdName:下游接口的方法名。
     * @param reqParamsStr:需要请求到下游的数据。
     * @return 直接返回下游的整个对象。
     */
    public static String commonInvoke(String className, 
                                      String mtdName, 
                                      String reqParamsStr) throws InvocationTargetException, IllegalAccessException, ClassNotFoundException {
        // 试图从类加载器中通过类名获取类信息对象
        Class<?> clz = CommonController.class.getClassLoader().loadClass(className);
        // 然后试图通过类信息对象想办法获取到该类对应的实例对象
        Object reqObj = tryFindBean(clz.getClass());
        
        // 通过反射找到 reqObj(例:userQueryFacade) 中的 mtdName(例:queryUserInfo) 方法
        Method reqMethod = ReflectionUtils.findMethod(clz, mtdName);
        // 并设置查找出来的方法可被访问
        ReflectionUtils.makeAccessible(reqMethod);
        
        // 将 reqParamsStr 反序列化为下游对象格式,并反射调用 invoke 方法
        Object resp =  reqMethod.invoke(reqObj, JSON.parseObject(reqParamsStr, reqMethod.getParameterTypes()[0]));
        
        // 判断响应对象的响应码,不是成功的话,则组装失败响应
        if(!SUCC.equals(OgnlUtils.getValue(resp, "respCode"))){
            return RespUtils.fail(resp);
        }
        // 如果响应码为成功的话,则组装成功响应
        return RespUtils.ok(resp);
    }
}

现在的关键问题就是tryFindBean方法中,我们该通过什么样的办法拿到服务调用接口的实例对象呢?或者说,该怎么仿照@DubboReference注解,拿到服务调用的实例对象呢?

  • 此时我们就需要使用到Dubbo提供的泛化调用特性了,即在调用方没有服务方提供的服务调用接口的情况下,对服务方进行调用,并且可以正常拿到调用结果。

泛化调用怎么用

环境准备:

  • 暴露的服务接口
public interface HelloService {
    String sayHello(String arg);
}
  • 提供服务接口的具体实现类,同时需要实现GenericService,表示为泛化调用
public class GenericImplOfHelloService implements GenericService,HelloService{
    @Override
    public Object $invoke(String method, String[] parameterTypes, Object[] args) throws GenericException {
        System.out.println("进行泛化调用");
        if(method.equals("sayHello")){
            return "泛化调用结果: "+sayHello(args[0].toString());
        }
        return null;
    }

    @Override
    public String sayHello(String arg) {
        return "hello "+arg;
    }
}

服务提供方使用API使用泛化调用的步骤:

  1. 在设置 ServiceConfig 时,使用setGeneric("true")来开启泛化调用
  2. 在设置 ServiceConfig 时,使用 setRef 指定实现类时,要设置一个 GenericService 的对象。而不是真正的服务实现类对象
  3. 其他设置与正常 Api 服务启动一致即可
  • 服务提供者完整代码
    @Test
    void genericProviderTest() throws InterruptedException {
        //创建ApplicationConfig
        ApplicationConfig applicationConfig = new ApplicationConfig();
        applicationConfig.setName("generic-impl-provider");
        //创建注册中心配置
        RegistryConfig registryConfig = new RegistryConfig();
        registryConfig.setAddress("zookeeper://8.134.144.48:2181");

        //新建服务实现类,注意要使用GenericService接收
        GenericService helloService = new GenericImplOfHelloService();

        //创建服务相关配置
        ServiceConfig<GenericService> service = new ServiceConfig<>();
        service.setApplication(applicationConfig);
        service.setRegistry(registryConfig);
        service.setInterface("dubbo.dubboSpi.HelloService");
        service.setRef(helloService);
        //重点:设置为泛化调用
        //注:不再推荐使用参数为布尔值的setGeneric函数
        //应该使用referenceConfig.setGeneric("true")代替
        service.setGeneric("true");
        service.export();

        System.out.println("dubbo service started");

        new CountDownLatch(1).await();
    }

服务消费方使用API使用泛化调用的步骤:

  1. 在设置 ReferenceConfig 时,使用 setGeneric("true") 来开启泛化调用
  2. 配置完 ReferenceConfig 后,使用 referenceConfig.get() 获取到 GenericService 类的实例
  3. 使用其 $invoke 方法获取结果
  4. 其他设置与正常 Api 服务启动一致即可
  • 服务消费者完整代码
    @Test
    void genericConsumerTest() throws InterruptedException {
        //创建ApplicationConfig
        ApplicationConfig applicationConfig = new ApplicationConfig();
        applicationConfig.setName("generic-call-consumer");
        //创建注册中心配置
        RegistryConfig registryConfig = new RegistryConfig();
        registryConfig.setAddress("zookeeper://8.134.144.48:2181");
        //创建服务引用配置
        ReferenceConfig<GenericService> referenceConfig = new ReferenceConfig<>();
        //设置接口
        referenceConfig.setInterface("dubbo.dubboSpi.HelloService");
        applicationConfig.setRegistry(registryConfig);
        referenceConfig.setApplication(applicationConfig);
        //重点:设置为泛化调用
        //注:不再推荐使用参数为布尔值的setGeneric函数
        //应该使用referenceConfig.setGeneric("true")代替
        referenceConfig.setGeneric(true);
        //设置异步,不必须,根据业务而定。
        referenceConfig.setAsync(true);
        //设置超时时间
        referenceConfig.setTimeout(7000);

        //获取服务,由于是泛化调用,所以获取的一定是GenericService类型
        GenericService genericService = referenceConfig.get();

        //使用GenericService类对象的$invoke方法可以代替原方法使用
        //第一个参数是需要调用的方法名
        //第二个参数是需要调用的方法的参数类型数组,为String数组,里面存入参数的全类名。
        //第三个参数是需要调用的方法的参数数组,为Object数组,里面存入需要的参数。
        Object result = genericService.$invoke("sayHello", new String[]{"java.lang.String"}, new Object[]{"world"});
        //使用CountDownLatch,如果使用同步调用则不需要这么做。
        CountDownLatch latch = new CountDownLatch(1);
        //获取结果
        CompletableFuture<String> future = RpcContext.getContext().getCompletableFuture();
        future.whenComplete((value, t) -> {
            System.err.println("invokeSayHello(whenComplete): " + value);
            latch.countDown();
        });
        //由于开启了异步模式,此处打印应该为null
        System.err.println("invokeSayHello(return): " + result);
        //打印结果
        latch.await();
    }
  • 测试
    在这里插入图片描述

通过Spring使用泛化调用

Spring 中服务暴露与服务发现有多种使用方式,如 xml,注解。这里以注解为例。 步骤:

  1. 生产者端无需改动
  2. 消费者端原有的 @DubboReference 注解上指明interfaceClass和generic=true
public interface UserService {
    User getUserById(String id);
}

public class UserServiceImpl implements UserService {
    @Override
    public User getUserById(String id) {
        // Do something to get the user by id
        return user;
    }
}
@Service
public class UserServiceImpl implements UserService {
    @DubboReference(interfaceClass = UserService.class, generic = true)
    private GenericService genericService;

    @Override
    public User getUserById(String id) {
        Object result = genericService.$invoke("getUserById", new String[]{"java.lang.String"}, new Object[]{id});
        return (User) result;
    }
}

利用泛化调用改造现有服务

Dubbo消费端发起远程调用的核心类是ReferenceConfig,接下来要做的就是拿到 referenceConfig#get 返回的泛化对象 GenericService,然后调用 GenericService#$invoke 方法进行远程调用。

@RestController
public class CommonController {
    // 响应码为成功时的值
    public static final String SUCC = "000000";
    
    // 定义URL地址
    @PostMapping("/gateway/{className}/{mtdName}/{parameterTypeName}/request")
    public String commonRequest(@PathVariable String className,
                                @PathVariable String mtdName,
                                @PathVariable String parameterTypeName,
                                @RequestBody String reqBody){
        // 将入参的req转为下游方法的入参对象,并发起远程调用
        return commonInvoke(className, parameterTypeName, mtdName, reqBody);
    }
    
    /**
     * 模拟公共的远程调用方法
     * @param className:下游的接口归属方法的全类名。
     * @param mtdName:下游接口的方法名。
     * @param parameterTypeName:下游接口的方法入参的全类名。
     * @param reqParamsStr:需要请求到下游的数据。
     * @return 直接返回下游的整个对象。
     */
    public static String commonInvoke(String className,
                                      String mtdName,
                                      String parameterTypeName,
                                      String reqParamsStr) {
        // 然后试图通过类信息对象想办法获取到该类对应的实例对象
        ReferenceConfig<GenericService> referenceConfig = createReferenceConfig(className);
        
        // 远程调用
        GenericService genericService = referenceConfig.get();
        Object resp = genericService.$invoke(
                mtdName,
                new String[]{parameterTypeName},
                new Object[]{JSON.parseObject(reqParamsStr, Map.class)});
        
        // 判断响应对象的响应码,不是成功的话,则组装失败响应
        if(!SUCC.equals(OgnlUtils.getValue(resp, "respCode"))){
            return RespUtils.fail(resp);
        }
        
        // 如果响应码为成功的话,则组装成功响应
        return RespUtils.ok(resp);
    }
    
    private static ReferenceConfig<GenericService> createReferenceConfig(String className) {
        DubboBootstrap dubboBootstrap = DubboBootstrap.getInstance();
        
        // 设置应用服务名称
        ApplicationConfig applicationConfig = new ApplicationConfig();
        applicationConfig.setName(dubboBootstrap.getApplicationModel().getApplicationName());
        
        // 设置注册中心的地址
        String address = dubboBootstrap.getConfigManager().getRegistries().iterator().next().getAddress();
        RegistryConfig registryConfig = new RegistryConfig(address);
        ReferenceConfig<GenericService> referenceConfig = new ReferenceConfig<>();
        referenceConfig.setApplication(applicationConfig);
        referenceConfig.setRegistry(registryConfig);
        referenceConfig.setInterface(className);
        
        // 设置泛化调用形式
        referenceConfig.setGeneric("true");
        // 设置默认超时时间5秒
        referenceConfig.setTimeout(5 * 1000);
        return referenceConfig;
    }
}
  • URL 地址增加了一个方法参数类名的维度,意味着通过类名、方法名、方法参数类名可以访问后台的提供者;
  • 通过接口类名来创建 ReferenceConfig 对象,并设置 generic = true 的核心属性;
  • 通过 referenceConfig.get 方法得到 genericService 泛化对象;
  • 将方法名、方法参数类名、业务请求参数传入泛化对象的 $invoke 方法中进行远程 Dubbo 调用,并返回响应对象;
  • 通过 Ognl 表达式语言从响应对象取出 respCode 响应码判断并做最终返回

泛化调用小结

泛化调用是指在调用方没有服务方提供的 API(SDK)的情况下,对服务方进行调用,并且可以正常拿到调用结果。

泛化采用一种统一的方式来发起对任何服务方法的调用,至少我们知道是一种接口调用的方式,只是这种方式有一个比较独特的名字而已。

泛化调用有哪些应用场景呢?

泛化调用主要用于实现一个通用的远程服务 Mock 框架,可通过实现 GenericService 接口处理所有服务请求。比如如下场景:

  • 网关服务:如果要搭建一个网关服务,那么服务网关要作为所有 RPC 服务的调用端。但是网关本身不应该依赖于服务提供方的接口 API(这样会导致每有一个新的服务发布,就需要修改网关的代码以及重新部署),所以需要泛化调用的支持。

  • 测试平台:如果要搭建一个可以测试 RPC 调用的平台,用户输入分组名、接口、方法名等信息,就可以测试对应的 RPC 服务。那么由于同样的原因(即会导致每有一个新的服务发布,就需要修改网关的代码以及重新部署),所以平台本身不应该依赖于服务提供方的接口 API。所以需要泛化调用的支持。

详细可以阅读官方文档: 泛化调用(客户端泛化)


Wrapper机制

到此为止,我们已经利用了泛化调用将服务消费端改造为了一个通用的网关服务,但是服务提供方这边如何处理泛化请求呢?

  • 泛化请求会携带接口类名、接口方法名、接口方法参数类名、业务请求参数,这四个维度的字段发起远程调用。
  • 服务提供方服务,需要在统一的入口中接收请求,然后派发到不同的接口服务中去。

如果要针对这个统一的入口进行编码实现,你会怎么写呢?

在这里插入图片描述
最容易想到的思路便是通过反射机制获取接口类名对应的类对象,然后利用类对象从IOC容器中拿到对应的bean,通过接口方法名和接口方法参数,来精准定位需要提供方接口服务中的哪个方法进行处理。

@RestController
public class CommonController {
    // 定义统一的URL地址
    @PostMapping("gateway/{className}/{mtdName}/{parameterTypeName}/request")
    public String recvCommonRequest(@PathVariable String className,
                                    @PathVariable String mtdName,
                                    @PathVariable String parameterTypeName,
                                    @RequestBody String reqBody) throws Exception {
        // 统一的接收请求的入口
        return commonInvoke(className, parameterTypeName, mtdName, reqBody);
    }

    /**
     * 统一入口的核心逻辑
     *
     * @param className:接口归属方法的全类名。
     * @param mtdName:接口的方法名。
     * @param parameterTypeName:接口的方法入参的全类名。
     * @param reqParamsStr:请求数据。
     * @return 接口方法调用的返回信息。
     */
    public static String commonInvoke(String className,
                                      String mtdName,
                                      String parameterTypeName,
                                      String reqParamsStr) throws Exception {
        // 通过反射机制可以获取接口类名对应的类对象
        Class<?> clz = Class.forName(className);
        // 接着通过类对象从IOC容器中定位对应的bean实例
        Object cacheObj = SpringCtxUtils.getBean(clz);
        // 通过反射找到方法对应的 Method 对象,并调用执行
        Class<?> methodParamType = Class.forName(parameterTypeName);
        Method method = clz.getDeclaredMethod(mtdName,methodParamType);
        method.setAccessible(true);
        return (String) method.invoke(cacheObj, JSON.parseObject(reqParamsStr,methodParamType));
    }
}

通过反射调用实现起来很简单,但是问题在于反射调用比较耗时,dubbo作为一款追求高性能的调用框架,并没有采用反射来实现,性能最高的实现应该是直接调用目标对象的方法,如下所示:

// 来精准定位需要提供方接口服务中的哪个方法进行处理
if ("sayHello".equals(mtdName) && String.class.getName().equals(parameterTypeName)) {
    // 真正的发起对源对象(被代理对象)的方法调用
    return ((DemoFacade) cacheObj).sayHello(reqParamsStr);
} else if("say".equals(mtdName) && Void.class.getName().equals(parameterTypeName)){
    // 真正的发起对源对象(被代理对象)的方法调用
    return ((DemoFacade) cacheObj).say();
}

很显然,我们无法直接将这段逻辑移到commonInvoke方法内部,因为我们不可能在commonInvoke方法内部使用if…else硬编码出所有情况,这实在是不合理 !

解决办法是将上面的逻辑移动到对应的服务提供实现类中,即每个服务实现类继承Dubbo提供的GenericService接口:

public interface GenericService {
    Object $invoke(String method, String[] parameterTypes, Object[] args) throws GenericException;
    ...
}

在接口的$invoke方法中硬编码列举所有可能的调用情况,如下所示:

public class GenericImplOfHelloService implements GenericService,HelloService{
    @Override
    public Object $invoke(String method, String[] parameterTypes, Object[] args) throws GenericException {
        if(method.equals("sayHello")){
            return "泛化调用结果: "+sayHello(args[0].toString());
        }
        return null;
    }

    @Override
    public String sayHello(String arg) {
        return "hello "+arg;
    }
}

利用Dubbo提供的泛化接口改造上面的代码,结果如下:

@RestController
public class CommonController {
    // 定义统一的URL地址
    @PostMapping("gateway/{className}/{mtdName}/{parameterTypeName}/request")
    public Object recvCommonRequest(@PathVariable String className,
                                    @PathVariable String mtdName,
                                    @PathVariable String parameterTypeName,
                                    @RequestBody String reqBody) throws Exception {
        // 统一的接收请求的入口
        return commonInvoke(className, parameterTypeName, mtdName, reqBody);
    }

    /**
     * 统一入口的核心逻辑
     *
     * @param className:接口归属方法的全类名。
     * @param mtdName:接口的方法名。
     * @param parameterTypeName:接口的方法入参的全类名。
     * @param reqParamsStr:请求数据。
     * @return 接口方法调用的返回信息。
     */
    public static Object commonInvoke(String className,
                                      String mtdName,
                                      String parameterTypeName,
                                      String reqParamsStr) throws Exception {
        // 通过反射机制可以获取接口类名对应的类对象
        Class<?> clz = Class.forName(className);
        // 接着通过类对象的简称获取到对应的接口服务
        GenericService genericService = SpringCtxUtils.getBean(clz);
        // 调用泛化接口的invoke方法
        return genericService.$invoke(mtdName,new String[]{parameterTypeName},new Object[]{JSON.parseObject(reqParamsStr,Class.forName(parameterTypeName))});
    }
}

目前看上去一切都很完美,唯一不完美的地方在于服务提供方的每个服务实现类都需要实现GenericService接口,并重写invoke方法,并在方法内部硬编码好相关的调用逻辑。

其实我们可以利用动态代理来将上面硬编码的重复逻辑抽取出来,动态代理常用的有JDK动态代理和Cglib动态代理,这里首先排除JDK动态代理,因为JDK动态代理采用的也是反射调用。

而Cglib 的核心原理,是通过执行拦截器的回调方法(methodProxy.invokeSuper),从代理类的众多方法引用中匹配正确方法,并执行被代理类的方法。

Cglib 的这种方式,就像代理类的内部动态生成了一堆的 if…else 语句来调用被代理类的方法,避免了手工写各种 if…else 的硬编码逻辑,省去了不少硬编码的活。

但是Dubbo没有采用cglib实现动态代理,因为Cglib的核心实现是生成了各种 if…else 代码来调用被代理类的方法,但是这块生成代理的逻辑不够灵活,难以自主修改。

Dubbo在 Cglib 的思想之上采用自主实现,并且不使用反射机制, 打造一个简化版的迷你型 Cglib 代理工具,这样一来,就可以在自己的代理工具中做各种与框架密切相关的逻辑了。


自定义代理

既然要自己生成代理类,就得先按照一个代码模板来编码,我们来设计代码模板:

public interface HelloService {
    String sayHello(String arg,String name);
    String test();
}


public class HelloServiceImpl implements HelloService{
    @Override
    public String sayHello(String arg, String name) {
        return "hello "+arg+" "+name;
    }

    @Override
    public String test() {
        return "test";
    }
}


//代理类模板
public class $GenericImplOfHelloService_1 extends HelloServiceImpl implements GenericService {
    public java.lang.Object $invoke(String method, String[] parameterTypes, Object[] args) throws GenericException {
        if ("test".equals(method) && (parameterTypes == null || parameterTypes != null && parameterTypes.length == 0)) {
            return test();
        }
        if ("sayHello".equals(method) && (parameterTypes != null && parameterTypes.length == 2 && parameterTypes[0].equals("java.lang.String") && parameterTypes[1].equals("java.lang.String"))) {
            return sayHello((java.lang.String) args[0], (java.lang.String) args[1]);
        }
        throw new GenericException(new NoSuchMethodException("Method [" + method + "] not found."));
    }
} 

有了代码模板,我们对照着代码模板用 Java 语言编写生成出来:

package com.provider.wrapperDemo;
import org.apache.dubbo.rpc.service.GenericException;
import org.apache.dubbo.rpc.service.GenericService;


import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;

public class CustomInvokerProxyUtils {
    private static final String WRAPPER_PACKAGE_NAME = "dubbo.dubboSpi";
    private static final AtomicInteger INC = new AtomicInteger();


    /**
     * 创建源对象(被代理对象)的代理对象
     */
    public static GenericService newProxyInstance(Object sourceTarget) throws Exception {
        // 代理类文件保存的磁盘路径
        String filePath = getWrapperBasePath();
        // 获取服务接口类
        Class<?> targetClazz = sourceTarget.getClass().getInterfaces()[0];
        // 生成的代理类名称:  $GenericImplOfHelloService_1
        String proxyClassName = "$GenericImplOf" + targetClazz.getSimpleName() + "_" + INC.incrementAndGet();
        // 获取代理的字节码内容
        String proxyByteCode = getProxyByteCode(proxyClassName, targetClazz, sourceTarget.getClass());
        // 缓存至磁盘中
        file2Disk(filePath, proxyClassName, proxyByteCode);
        // 等刷盘稳定后
        Thread.sleep(2000);
        // 再编译java加载class至内存中,返回实例化的对象
        return (GenericService) compileJava2Class(filePath, proxyClassName);
    }

    private static String getWrapperBasePath() {
        return Objects.requireNonNull(CustomInvokerProxyUtils.class.getResource("/")).getPath()
                + CustomInvokerProxyUtils.class.getPackage().toString().substring("package ".length()).replaceAll("\\.", "/");
    }

    /**
     * 生成代理的字节码内容,其实就是一段类代码的字符串
     */
    private static String getProxyByteCode(String proxyClassName, Class<?> targetClazz, Class<?> sourceClass) {
        StringBuilder sb = new StringBuilder();
        // java文件第一行是package声明包路径
        String pkgContent = "package " + CustomInvokerProxyUtils.WRAPPER_PACKAGE_NAME + ";";
        //通过import导入代理类中可能会使用到的类
        String importTargetClazzImpl = "import " + sourceClass.getName() + ";";
        String importGenericService = "import " + GenericService.class.getName() + ";";
        String importGenericException = "import " + GenericException.class.getName() + ";";
        String importNoSuchMethodException="import "+ NoSuchMethodException.class.getName()+";";
        // 代理类主体内容构建
        String classHeadContent = "public class " + proxyClassName + " extends " + sourceClass.getSimpleName() + " implements " + GenericService.class.getSimpleName() + " {";
        // 添加内容
        sb.append(pkgContent).append(importTargetClazzImpl).append(importGenericService).append(importGenericException)
                .append(importNoSuchMethodException).append(classHeadContent);
        // 构建invoke方法
        String invokeMethodHeadContent = "public " + Object.class.getName() + " $invoke" +
                "(" + String.class.getSimpleName() + " method, "
                + String.class.getSimpleName() + "[] parameterTypes, "
                + Object.class.getSimpleName() + "[] args) throws " + GenericException.class.getSimpleName() + " {\n";
        sb.append(invokeMethodHeadContent);
        // 组装if...else...逻辑
        for (Method method : targetClazz.getMethods()) {
            String methodName = method.getName();
            Class<?>[] parameterTypes = method.getParameterTypes();
            String ifHead = "if (\"" + methodName + "\".equals(method)"+buildMethodParamEqual(parameterTypes)+") {\n";
            //组装方法调用逻辑
            String ifContent = buildMethodInvokeContent(methodName, parameterTypes);
            String ifTail = "}\n";
            sb.append(ifHead).append(ifContent).append(ifTail);
        }
        // throw new GenericException("Method [" + method + "] not found.");
        String invokeMethodTailContent = "throw new " + GenericException.class.getSimpleName() + "(new NoSuchMethodException(\"Method [\" + method + \"] not found.\"));\n}\n";
        sb.append(invokeMethodTailContent);
        // 类的尾巴大括号
        String classTailContent = " } ";
        sb.append(classTailContent);
        return sb.toString();
    }

    private static String buildMethodParamEqual(Class<?>[] parameterTypes) {
        StringBuilder methodParamEqualContent=new StringBuilder();
        methodParamEqualContent.append("&&(");
        //方法参数为0,则可以传入null
        if(parameterTypes.length==0){
            methodParamEqualContent.append("parameterTypes==null ||");
        }
        //参数类型必须合法
        methodParamEqualContent.append(" parameterTypes!=null&&parameterTypes.length==").append(parameterTypes.length);
        for (int i = 0; i < parameterTypes.length; i++) {
            methodParamEqualContent.append("&&parameterTypes[").append(i).append("].equals(\"").append(parameterTypes[i].getName()).append("\")");
        }
        methodParamEqualContent.append(")");
        return methodParamEqualContent.toString();
    }

    private static String buildMethodInvokeContent(String methodName, Class<?>[] parameterTypes) {
        if (parameterTypes.length == 0) {
            return "return " + methodName + "();\n";
        }
        StringBuilder methodInvokeContent = new StringBuilder();
        methodInvokeContent.append("return ").append(methodName).append("(");
        for (int i = 0; i < parameterTypes.length; i++) {
            methodInvokeContent.append("(").append(parameterTypes[i].getName()).append(")")
                    .append("args[").append(i).append("]").append(",");
        }
        //删除最后一个多余的,
        methodInvokeContent.delete(methodInvokeContent.length()-1,methodInvokeContent.length());
        methodInvokeContent.append(");\n");
        return methodInvokeContent.toString();
    }

    private static void file2Disk(String filePath, String proxyClassName, String proxyByteCode) throws IOException {
        File file = new File(filePath + File.separator + proxyClassName + ".java");
        if (!file.exists()) {
            file.createNewFile();
        }
        FileWriter fileWriter = new FileWriter(file);
        fileWriter.write(proxyByteCode);
        fileWriter.flush();
        fileWriter.close();
    }

    private static Object compileJava2Class(String filePath, String proxyClassName) throws Exception {
        // 编译 Java 文件
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);
        Iterable<? extends JavaFileObject> compilationUnits =
                fileManager.getJavaFileObjects(new File(filePath + File.separator + proxyClassName + ".java"));
        JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, null, null, null, compilationUnits);
        task.call();
        fileManager.close();
        // 加载 class 文件
        URL[] urls = new URL[]{new URL("file:" + filePath)};
        URLClassLoader urlClassLoader = new URLClassLoader(urls);

        Class<?> clazz = urlClassLoader.loadClass(CustomInvokerProxyUtils.WRAPPER_PACKAGE_NAME + "." + proxyClassName);
        // 反射创建对象,并且实例化对象
        Constructor<?> constructor = clazz.getConstructor();
        return constructor.newInstance();
    }
}

生成的代码主要有三个步骤:

  • 按照代码模板的样子,使用 Java 代码动态生成出来一份代码字符串。
  • 将生成的代码字符串保存到磁盘中。
  • 根据磁盘文件路径将文件编译为 class 文件,然后利用 URLClassLoader 加载至内存变成 Class 对象,最后反射创建对象并且实例化对象。

注意: 如果抛出下面这个异常

 Caused by: java.lang.ClassNotFoundException: com.sun.tools.javac.processing.JavacProcessingEnvironment

可能是因为缺少 tools.jar 这个依赖,可以在maven中引入相关依赖:

<dependency>
  <groupId>com.sun</groupId>
  <artifactId>tools</artifactId>
  <version>1.8.0</version>
  <scope>system</scope>
  <systemPath>${java.home}/../lib/tools.jar</systemPath>
</dependency>

请注意,system 作用域将使 Maven 不会从远程仓库中下载这个依赖,而是使用指定的 systemPath。你需要根据你的环境修改 systemPath 的值,确保它指向 tools.jar 的实际路径。

测试:

    @Test
    void wrapperDemoTest() throws Exception {
        GenericService genericService = CustomInvokerProxyUtils.newProxyInstance(new HelloServiceImpl());
        Object testMethodRes = genericService.$invoke("test", null, null);
        System.out.println("test method invoke res: "+testMethodRes);
        Object sayHelloRes = genericService.$invoke("sayHello", new String[]{String.class.getName(),String.class.getName()}, new Object[]{"参数", "大忽悠"});
        System.out.println("sayHello method invoke res: "+sayHelloRes);
    }

在这里插入图片描述

生成的java文件:
在这里插入图片描述


dubbo底层wrapper原理

上面通过代理类实现GenericService泛化接口,是我根据Dubbo官方文档泛化调用(客户端泛化)一节,服务提供者端代码启发而作,dubbo官方实现思路类似,但是代理类并非实现GenericService接口,下面我们一起来看看。

dubbo创建的代理类继承了Wrapper接口,实现类比上面给出的GenericService:

// org.apache.dubbo.rpc.proxy.javassist.JavassistProxyFactory#getInvoker
// 创建一个 Invoker 的包装类
@Override
public <T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) {
    // 这里就是生成 Wrapper 代理对象的核心一行代码
    final Wrapper wrapper = Wrapper.getWrapper(proxy.getClass().getName().indexOf('$') < 0 ? proxy.getClass() : type);
    // 包装一个 Invoker 对象
    return new AbstractProxyInvoker<T>(proxy, type, url) {
        @Override
        protected Object doInvoke(T proxy, String methodName,
                                  Class<?>[] parameterTypes,
                                  Object[] arguments) throws Throwable {
            // 使用 wrapper 代理对象调用自己的 invokeMethod 方法
            // 以此来避免反射调用引起的性能开销
            // 通过强转来实现统一方法调用
            return wrapper.invokeMethod(proxy, methodName, parameterTypes, arguments);
        }
    };
}

使用Dubbo提供的Wrapper创建代理对象,并进行方法调用演示:

    @Test
    void dubboWrapperTest() throws InvocationTargetException {
        HelloService helloService = new HelloServiceImpl();
        final Wrapper wrapper = Wrapper.getWrapper(helloService.getClass());
        Object testMethodRes = wrapper.invokeMethod(helloService, "test", new Class[]{},null);
        System.out.println("test method invoke res: " + testMethodRes);
        Object sayHelloRes = wrapper.invokeMethod(helloService,"sayHello",new Class[]{String.class, String.class}, new Object[]{"参数", "大忽悠"});
        System.out.println("sayHello method invoke res: " + sayHelloRes);
    }

在这里插入图片描述
我们把生成的 wrapper 代理类 class 文件反编译为 Java 代码,看看生成的内容到底长什么样的,这里需要借助阿里提供的Arthas工具完成:

  1. 下载Arthas: curl -O https://arthas.aliyun.com/arthas-boot.jar
  2. 启动测试用例,并在测试方法结尾调用System.in.read()方法挂起当前线程
  3. 启动Arthas: java -jar arthas-boot.jar
    在这里插入图片描述
  4. 模糊搜索所有dubbo生成的代理类

在这里插入图片描述
5. 查看对应代理类的完整代码

在这里插入图片描述

package dubbo.dubboSpi;

import dubbo.dubboSpi.HelloServiceImpl;
import java.lang.reflect.InvocationTargetException;
import java.util.Map;
import org.apache.dubbo.common.bytecode.ClassGenerator;
import org.apache.dubbo.common.bytecode.NoSuchMethodException;
import org.apache.dubbo.common.bytecode.NoSuchPropertyException;
import org.apache.dubbo.common.bytecode.Wrapper;

public class HelloServiceImplDubboWrap0 extends Wrapper implements ClassGenerator.DC {
    public static String[] pns;
    public static Map pts;
    public static String[] mns;
    public static String[] dmns;
    public static Class[] mts0;
    public static Class[] mts1;

    @Override
    public String[] getPropertyNames() {
        return pns;
    }

    @Override
    public boolean hasProperty(String string) {
        return pts.containsKey(string);
    }

    public Class getPropertyType(String string) {
        return (Class)pts.get(string);
    }

    @Override
    public String[] getMethodNames() {
        return mns;
    }

    @Override
    public String[] getDeclaredMethodNames() {
        return dmns;
    }

    @Override
    public void setPropertyValue(Object object, String string, Object object2) {
        try {
            HelloServiceImpl helloServiceImpl = (HelloServiceImpl)object;
        }
        catch (Throwable throwable) {
            throw new IllegalArgumentException(throwable);
        }
        throw new NoSuchPropertyException(new StringBuffer().append("Not found property \"").append(string).append("\" field or setter method in class dubbo.dubboSpi.HelloServiceImpl.").toString());
    }

    @Override
    public Object getPropertyValue(Object object, String string) {
        try {
            HelloServiceImpl helloServiceImpl = (HelloServiceImpl)object;
        }
        catch (Throwable throwable) {
            throw new IllegalArgumentException(throwable);
        }
        throw new NoSuchPropertyException(new StringBuffer().append("Not found property \"").append(string).append("\" field or getter method in class dubbo.dubboSpi.HelloServiceImpl.").toString());
    }

    public Object invokeMethod(Object object, String string, Class[] classArray, Object[] objectArray) throws InvocationTargetException {
        HelloServiceImpl helloServiceImpl;
        try {
            helloServiceImpl = (HelloServiceImpl)object;
        }
        catch (Throwable throwable) {
            throw new IllegalArgumentException(throwable);
        }
        try {
            if ("sayHello".equals(string) && classArray.length == 2) {
                return helloServiceImpl.sayHello((String)objectArray[0], (String)objectArray[1]);
            }
            if ("test".equals(string) && classArray.length == 0) {
                return helloServiceImpl.test();
            }
        }
        catch (Throwable throwable) {
            throw new InvocationTargetException(throwable);
        }
        throw new NoSuchMethodException(new StringBuffer().append("Not found method \"").append(string).append("\" in class dubbo.dubboSpi.HelloServiceImpl.").toString());
    }
}

上面dubbo生成的动态代理类中,我们暂时只需要重点关注invokeMethod方法逻辑即可,可以看出其实和我们上面所讲的逻辑是一致的。

wrapper类源码本文不做展开,后续章节再深入分析。


小结

我们从服务提供方设计统一入口为题进行切入,从反射调用改造,到尝试硬编码提到性能,从而引出了自定义动态代理,虽然Cglib代理实现逻辑符合改造诉求,但是对于定制生成代理类的灵活需求,还得受Cglib库的限制。

因此,考虑上诉因素后,dubbo自定义了一个迷你型的Cglib代理工具,总体实现思路为:

  1. 先设计出一套通用的代码模板,使其具备业务场景的通用性,方便进行统一代理
  2. 通过手写java代码或者通过字节码工具,按照代码模板要求生成一套动态的代码
  3. 最后,将动态生成的代码通过JDK编译或者通过字节码工具,最终想办法生成Class对象
  4. 然后拿着生成的Class对象创建一个实例,用实例对象进行方法调用

小结

本文参考Dubbo官网提供的源码解析部分,综合个人理解,如有错误,欢迎评论区指出,或者私信与我讨论。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/496929.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

java实现大气污染排放传输路径模拟(iClientOpenlayer前端渲染)

开头先看下模拟实现效果图 一、技术应用及背景说明 了解大气污染传输路径模拟可以帮助我们更好地了解空气污染的来源和传播方式&#xff0c;从而采取更有效的控制措施。这种模拟技术可以根据大气环境和气象条件&#xff0c;模拟出污染物在大气中的传播路径和影响范围&#xff0…

树的储存结构和表示法_20230506

树的储存结构和表示法 前言 树是一类非常重要的数据结构&#xff0c;它是图和其它更高阶数据的基础&#xff0c;人们对树的储存结构和表示法进行了大量研究&#xff0c;这里介绍三种常见的链表结构来表示树的基本方法。 树的双亲表示法 假设以一组连续空间储存数据的结点&a…

MySQL基础(六)多表查询

多表查询&#xff0c;也称为关联查询&#xff0c;指两个或更多个表一起完成查询操作。 前提条件&#xff1a;这些一起查询的表之间是有关系的&#xff08;一对一、一对多&#xff09;&#xff0c;它们之间一定是有关联字段&#xff0c;这个关联字段可能建立了外键&#xff0c;…

密码学【java】初探究加密方式之非对称加密

文章目录 非对称加密1 常见算法2 生成公钥和私钥3 私钥加密4 私钥加密 公钥解密5 公钥和私钥的保存和读取5.1 **保存公钥和私钥**5.2 读取公钥和私钥 非对称加密 非对称加密算法又称现代加密算法。非对称加密是计算机通信安全的基石&#xff0c;保证了加密数据不会被破解。与对…

argument type mismatch

后端接收前端传来的数据 id&#xff0c;进行批量删除&#xff0c;报错如下&#xff1a; 错误一 removeByIds(ids) com.baomidou.mybatisplus.core.exceptions.MybatisPlusException: java.lang.IllegalArgumentException: argument type mismatch at com.baomidou.mybati…

华为OD机试 - 各位相加(Java)

一、题目描述 给定一个非负整数 num&#xff0c;反复将各个位上的数字相加&#xff0c;直到结果为一位数。 二、思路与算法 各位相加&#xff0c;使用递归&#xff0c;出口是结果的长度等于1。 三、Java算法源码 public static int addDigits(int num) {recursion(num);re…

vs2017如何创建一个asax文件

VS2017无法为网站创建Global.asax文件&#xff0c;导致出现错误WebForms UnobtrusiveValidationMode 需要“jquery”ScriptResourceMapping。 解决方案如下&#xff1a; 勾选要应用的网站&#xff0c;这里我要应用的是ExSite 点击安装&#xff0c;然后点击确定即可。 此时&am…

单片机+PHY芯片+Powerlink协议实现高效数据采集探究(工业总线485和CAN的升级)

CAN总线和RS-485总线作为常用的工业通信总线&#xff0c;在许多工业领域中得到广泛使用。但随着工业应用的不断扩展和网络化的需求增加&#xff0c;它们面临着一些局限性。例如CAN总线虽然具有较高的通信速率和可靠性&#xff0c;但存在节点数量受限、数据传输距离短等问题。而…

Nginx总结

目录 Nginx介绍 Nginx的作用 反向代理 项目架构 实战&#xff1a;访问nginx服务器反向代理到另一台虚拟机上的tomcat服务器 负载均衡 项目架构 实战&#xff1a;访问nginx服务器&#xff0c;是否反向代理到集群中的任意一台tomcat服务器&#xff0c;停止一台tomcat服务器&…

JAVA16新特性

JAVA16新特性 概述 2021年3月16日正式发布,一共更新了17JEP https://openjdk.java.net/projects/jdk/16/ 一 语法层面 1_JEP 397&#xff1a;密封类&#xff08;第二次预览&#xff09; sealed class 第二次预览 通过密封的类和接口来增强Java编程语言,这是新的预览特性,用…

stream的collectors

起因的话&#xff0c;新进公司&#xff0c;看见了一段有意思的代码。 public final class MyCollectors {private MyCollectors() {}static final Set<Collector.Characteristics> CH_ID Collections.unmodifiableSet(EnumSet.of(Collector.Characteristics.IDENTITY_F…

开源中国面试准备

dockerFile常见命令 1、FROM 设置要制作的镜像基于哪个镜像&#xff0c;FROM指令必须是整个Dockerfile的第一个指令&#xff0c;如果指定的镜像不存在默认会自动从Docker Hub上下载 2、MAINTAINER 镜像作者的信息&#xff0c;比如名字或邮箱地址 语法&#xff1a;MAINTAINER n…

HTTP第三讲——四层模型、七层模型

四层模型 TCP/IP 协议&#xff0c;它是 HTTP 协议的下层协议&#xff0c;负责具体的数据传输 工作。TCP/IP 协议是一个“有层次的协议栈”。 TCP/IP 当初的设计者真的是非常聪明&#xff0c;创造性地提出了“分层”的概念&#xff0c;把复杂的网络通信划分出多个层次&#xff…

免费矢量图标网站有哪些?

图标作为UI设计的必要元素&#xff0c;矢量图标是质量的保证。据说完美的用户体验应该从灵活性开始 。在响应设计盛行的当下&#xff0c;灵活矢量图标的重要性不言而喻。在这种情况下&#xff0c;风格齐全、质量上乘的矢量图标网站堪称设计宝藏。在这篇文章中&#xff0c;我们…

基于springboot的医院信管系统

摘 要 随着信息技术和网络技术的飞速发展&#xff0c;人类已进入全新信息化时代&#xff0c;传统管理技术已无法高效&#xff0c;便捷地管理信息。为了迎合时代需求&#xff0c;优化管理效率&#xff0c;各种各样的管理系统应运而生&#xff0c;各行各业相继进入信息管理时代&…

读书笔记--数据治理之法

继续延续上一篇文章&#xff0c;对数据治理之法进行学习。数据治理之法是战术层面的方法&#xff0c;是一套涵盖8项举措的数据治理实施方法论&#xff0c;包括梳理现状与确定目标、能力成熟度评估、治理路线图规划、保障体系建设、技术体系建设、治理策略执行与监控、绩效考核与…

c++学习:STL之string类初识

目录 1.关于STL 1.什么是STL 2.STL的六的组件 2.关于string类的学习 1.为何学习string类 2.何为string类 3.string类对象的构造 4.容量操作 5.元素访问的操作 6.迭代器&#xff08;Iterators&#xff09; 7.修改类的操作 8.字符串操作 1.关于STL 1.什么是STL STL&a…

理解多态的实现原理

目录 概念&#xff1a; 特例&#xff1a; 协变&#xff1a; final&#xff1a; override: 抽象类、纯虚函数&#xff1a; 查表&#xff1a; 动态绑定&#xff1a; 概念&#xff1a; 就是多个不同的对象&#xff0c;在完成某种相同动作时&#xff0c;会产生多种不同的状态…

佩戴更舒适的骨传导耳机,音质也很出色,南卡 NEO体验

现在天气越来越好&#xff0c;特别适合户外运动&#xff0c;很多人跑步健身时都喜欢戴上一副骨传导耳机&#xff0c;听音乐的同时&#xff0c;还可以随时留意周围的交通状况。在国产的骨传导耳机里面&#xff0c;南卡是很受欢迎的一个品牌&#xff0c;而且旗下产品非常丰富&…

基于Redis优化验证码登录流程, 解决登录状态刷新问题

文章目录 1 问题: 多台Tomcat间session共享问题2 Redis代替session的业务流程分析2.1 设计key的结构2.2 设计Key的具体细节2.3 整体访问流程 3 基于Redis实现短信登录4 解决状态登录刷新问题4.1 初始方案问题4.2 优化方案4.3 代码 1 问题: 多台Tomcat间session共享问题 书接上…