从源码全面解析 dubbo 服务暴露的来龙去脉

news2024/10/6 4:07:15
  • 👏作者简介:大家好,我是爱敲代码的小黄,独角兽企业的Java开发工程师,CSDN博客专家,阿里云专家博主
  • 📕系列专栏:Java设计模式、Spring源码系列、Netty源码系列、Kafka源码系列、JUC源码系列
  • 🔥如果感觉博主的文章还不错的话,请👍三连支持👍一下博主哦
  • 🍂博主正在努力完成2023计划中:以梦为马,扬帆起航,2023追梦人
  • 📝联系方式:hls1793929520,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬👀、

在这里插入图片描述

文章目录

    • 一、引言
    • 二、环境配置
    • 三、服务暴露
      • 1、判断注册方式
        • 1.1 获取注册的URL
        • 1.2 应用注册
        • 1.3 接口注册
      • 2、服务导出
        • 2.1 服务暴露
        • 2.2 动态代理生成
        • 2.3 服务导出
          • 2.3.1 服务启动
          • 2.3.2 注册Zookeeper
      • 3、疑惑解答
    • 四、总结

一、引言

对于 Java 开发者而言,关于 dubbo ,我们一般当做黑盒来进行使用,不需要去打开这个黑盒。

但随着目前程序员行业的发展,我们有必要打开这个黑盒,去探索其中的奥妙。

本期 dubbo 源码解析系列文章,将带你领略 dubbo 源码的奥秘

本期源码文章吸收了之前 SpringKakfaJUC源码文章的教训,将不再一行一行的带大家分析源码,我们将一些不重要的部分当做黑盒处理,以便我们更快、更有效的阅读源码。

虽然现在是互联网寒冬,但乾坤未定,你我皆是黑马!

废话不多说,发车!

二、环境配置

本篇文章适合对 dubbo 有兴趣 & 日常工作中有使用的人

环境配置:

  • dubbo版本:3.1.8
  • maven版本:3.5.4
  • JDK版本:JDK8
  • Zookeeper版本:3.4.9

因为服务数据是注册在 Zookeeper 上的,所以需要一个 Zookeeper 的可视化界面:ZooInspector

当然,就算你上述环境配置不全,也不影响你本篇文章的阅读体验。

三、服务暴露

上一篇文章《从源码全面解析dubbo服务注册的来龙去脉》 我们分析了我们的 Dubbo 是如何解析 @EnableDubboConfig@DubboComponentScan 这两个注解的

我们留了一个 服务暴露 的过程没有讲。因为 服务暴露Dubbo 中属于比较重要的知识点,所以单独一篇来进行讲解

我们上一篇文章讲到在 DubboDeployApplicationListener 里面实现了 服务暴露 的过程

我们主要讲一下 DubboDeployApplicationListener 的实现:

public class DubboDeployApplicationListener implements ApplicationListener<ApplicationContextEvent>, ApplicationContextAware, Ordered {
    @Override
    public void onApplicationEvent(ApplicationContextEvent event) {
        if (nullSafeEquals(applicationContext, event.getSource())) {
            // 判断当前的事件
            if (event instanceof ContextRefreshedEvent) {
                // 刷新事件
                onContextRefreshedEvent((ContextRefreshedEvent) event);
            } else if (event instanceof ContextClosedEvent) {
                // 关闭事件
                onContextClosedEvent((ContextClosedEvent) event);
            }
        }
    }
}

private void onContextRefreshedEvent(ContextRefreshedEvent event) {
    ModuleDeployer deployer = moduleModel.getDeployer();
    Future future = deployer.start();
}

public Future start() throws IllegalStateException {
    // 初始化模块
    applicationDeployer.initialize();
    return startSync();
}

private synchronized Future startSync() throws IllegalStateException {
    // 服务暴露
    exportServices();
}

1、判断注册方式

由于我们这个是 Dubbo3 的源码,Dubbo3 新增了一种注册方式:应用级注册,所以在这里会判断当前的注册方式是哪一种

  • dubbo2:接口级注册
  • dubbo3:应用级注册

在这里插入图片描述

我们看一下 exportServices 是如何对其进行处理的

直接来到 org.apache.dubbo.config.ServiceConfigdoExport 方法

protected synchronized void doExport() {
    // 将一个服务暴露成多个URL
    doExportUrls();
    exported();
}

1.1 获取注册的URL

private void doExportUrls() {
    // 获取当前
    List<URL> registryURLs = ConfigValidationUtils.loadRegistries(this, true);
    // 省略部分代码
}

public static List<URL> loadRegistries(AbstractInterfaceConfig interfaceConfig, boolean provider) {
    // 获取配置里面的地址
    String address = config.getAddress();
    if (StringUtils.isEmpty(address)) {
        address = ANYHOST_VALUE;
    }
    
    // 若配置多个URL地址,在这里切割
    List<URL> urls = UrlUtils.parseURLs(address, map);
    
    // 填充URL多个地址
    for (URL url : urls) {
        // 填充URL地址
        url = URLBuilder.from(url)
            .addParameter(REGISTRY_KEY, url.getProtocol())
        	.setProtocol(extractRegistryType(url))
        	.setScopeModel(interfaceConfig.getScopeModel())
        	.build();
        // 判断当前是不是服务端,将URL添加至registryList
        if (provider || url.getParameter(SUBSCRIBE_KEY, true)) {
            registryList.add(url);
        }
        // 这里的URL示例:
        // registry://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService?application=dubbo-provider&dubbo=2.0.2&metadata-type=remote&pid=13064&qos.enable=true&register-mode=instance&registry=zookeeper&release=3.1.8&timestamp=1685375979873
    }
    
    // 根据注册类型拼接URL配置
    return genCompatibleRegistries(interfaceConfig.getScopeModel(), registryList, provider);
}

1.2 应用注册

  • 当前 registerMode(注册类型)instance 或者 all 时,走应用注册
if ((DEFAULT_REGISTER_MODE_INSTANCE.equalsIgnoreCase(registerMode) || DEFAULT_REGISTER_MODE_ALL.equalsIgnoreCase(registerMode))
    && registryNotExists(registryURL, registryList, SERVICE_REGISTRY_PROTOCOL)) {
    URL serviceDiscoveryRegistryURL = URLBuilder.from(registryURL)
        .setProtocol(SERVICE_REGISTRY_PROTOCOL)
        .removeParameter(REGISTRY_TYPE_KEY)
        .build();
    result.add(serviceDiscoveryRegistryURL);
}
  • URL:

    service-discovery-registry://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService?application=dubbo-provider&dubbo=2.0.2&metadata-type=remote&pid=13064&qos.enable=true&register-mode=instance&registry=zookeeper&release=3.1.8&timestamp=1685375979873
    

1.3 接口注册

  • 当前 registerMode(注册类型)interface 或者 all 时,走接口注册

    if (DEFAULT_REGISTER_MODE_INTERFACE.equalsIgnoreCase(registerMode) || DEFAULT_REGISTER_MODE_ALL.equalsIgnoreCase(registerMode)) {
        result.add(registryURL);
    }
    
  • URL

    registry://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService?application=dubbo-provider&dubbo=2.0.2&metadata-type=remote&pid=13064&qos.enable=true&register-mode=instance&registry=zookeeper&release=3.1.8&timestamp=1685375979873
    

2、服务导出

在这里插入图片描述

// 遍历当前的传输协议(dubbo、rest、tri)
for (ProtocolConfig protocolConfig : protocols) {
    // 组装当前的接口URL:com.common.service.IUserService:1.0.0.dev
    String pathKey = URL.buildKey(getContextPath(protocolConfig).map(p -> p + "/" + path).orElse(path), group, version);
    // 正式导出服务
    doExportUrlsFor1Protocol(protocolConfig, registryURLs);
}

private void doExportUrlsFor1Protocol(ProtocolConfig protocolConfig, List<URL> registryURLs) {
    // 获取数据配置
	Map<String, String> map = buildAttributes(protocolConfig);
    serviceMetadata.getAttachments().putAll(map);

    // 组装URL
    URL url = buildUrl(protocolConfig, map);
	
    // 服务暴露
    exportUrl(url, registryURLs);
}
  • 组装的URL:

    dubbo://192.168.0.103:20883/com.common.service.IUserService?anyhost=true&application=dubbo-provider&background=false&bind.ip=192.168.0.103&bind.port=20883&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=com.msb.common.service.IUserService&metadata-type=remote&methods=getUserById&pid=1168&qos.enable=true&register-mode=instance&release=3.1.8&revision=1.0.0.dev&side=provider&timeout=100&timestamp=1685377125239&version=1.0.0.dev
    

2.1 服务暴露

private void exportUrl(URL url, List<URL> registryURLs) {
    exportLocal(url);
}

private void exportLocal(URL url) {
    // 组装配置
    URL local = URLBuilder.from(url)
        .setProtocol(LOCAL_PROTOCOL)
        .setHost(LOCALHOST_VALUE)
        .setPort(0)
        .build();
    local = local.setScopeModel(getScopeModel())
        .setServiceModel(providerModel);
    
    // 服务暴露
    doExportUrl(local, false);
}

private void doExportUrl(URL url, boolean withMetaData) {
    // 生成动态代理
    Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, url);
    if (withMetaData) {
        invoker = new DelegateProviderMetaDataInvoker(invoker, this);
    }
    // 服务导出
    Exporter<?> exporter = protocolSPI.export(invoker);
    exporters.add(exporter);
}

2.2 动态代理生成

这个里面的 getIUnvoker 很经典的动态代理模式,当我们客户端调用时,会调用 doInvoke 里面的方法

public <T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) {
    final Wrapper wrapper = Wrapper.getWrapper(proxy.getClass().getName().indexOf('$') < 0 ? proxy.getClass() : type);
    return new AbstractProxyInvoker<T>(proxy, type, url) {
        @Override
        protected Object doInvoke(T proxy, String methodName,Class<?>[] parameterTypes,Object[] arguments) {
            // proxy:UserServicelmpl
            // 直接调用实现类
            return wrapper.invokeMethod(proxy, methodName, parameterTypes, arguments);
        }
    };
} 

2.3 服务导出

Exporter<?> exporter = protocolSPI.export(invoker);

public <T> Exporter<T> export(final Invoker<T> originInvoker) throws RpcException {

    // 暴露服务
    final ExporterChangeableWrapper<T> exporter = doLocalExport(originInvoker, providerUrl);

    // 获取注册的Registry
    final Registry registry = getRegistry(registryUrl);
    final URL registeredProviderUrl = getUrlToRegistry(providerUrl, registryUrl);

    // 注册方式的选择
    boolean register = providerUrl.getParameter(REGISTER_KEY, true) && registryUrl.getParameter(REGISTER_KEY, true);
    if (register) {
        // 这里有两种注册情况:
        // 1、接口注册:直接将注册数据注册到Zookeeper即可
        // 2、应用注册:将注册数据转换成元数据等后面发布元数据
        register(registry, registeredProviderUrl);
    }
}
2.3.1 服务启动

我们都知道 dubbo 都是自定义的端口,比如上面的我们的 20883,这个端口哪里来的呢?

相信有部分同学可能猜到答案了,没错,就是 Netty 启动的

final ExporterChangeableWrapper<T> exporter = doLocalExport(originInvoker, providerUrl);

我们直接跳转到 DubboProtocolexport 方法

public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
    // 启动服务
    openServer(url);
    optimizeSerialization(url);

    return exporter;
}

private void openServer(URL url) {
    checkDestroyed();
    String key = url.getAddress();
    boolean isServer = url.getParameter(IS_SERVER_KEY, true);

    if (isServer) {
        ProtocolServer server = serverMap.get(key);
        // 典型的双端检锁机制
        if (server == null) {
            synchronized (this) {
                server = serverMap.get(key);
                if (server == null) {
                    // createServer:启动服务
                    serverMap.put(key, createServer(url));
                    return;
                }
            }
        }

        // 出现问题重置服务配置
        server.reset(url);
    }
}

创建服务端:

private ProtocolServer createServer(URL url) {
    // 数据组装
    url = URLBuilder.from(url)
        .addParameterIfAbsent(CHANNEL_READONLYEVENT_SENT_KEY, Boolean.TRUE.toString())
        .addParameterIfAbsent(HEARTBEAT_KEY, String.valueOf(DEFAULT_HEARTBEAT))
        .addParameter(CODEC_KEY, DubboCodec.NAME)
        .build();

    String transporter = url.getParameter(SERVER_KEY, DEFAULT_REMOTING_SERVER);

    ExchangeServer server = Exchangers.bind(url, requestHandler);
}

直接跳到 NettyServerdoOpen 方法

protected void doOpen() throws Throwable {
    bootstrap = new ServerBootstrap();

    bossGroup = createBossGroup();
    workerGroup = createWorkerGroup();

    final NettyServerHandler nettyServerHandler = createNettyServerHandler();
    channels = nettyServerHandler.getChannels();

    initServerBootstrap(nettyServerHandler);

    ChannelFuture channelFuture = bootstrap.bind(getBindAddress());
    channelFuture.syncUninterruptibly();
    channel = channelFuture.channel();

}
2.3.2 注册Zookeeper

我们这里直接跳到 ZookeeperRegistrydoRegister 方法

public void doRegister(URL url) {
    // 校验
    checkDestroyed();
    // 将接口信息注册至Zookeeper
    zkClient.create(toUrlPath(url), url.getParameter(DYNAMIC_KEY, true), false);
}

// URL:dubbo://192.168.0.103:20883/com.common.service.IUserService?anyhost=true&application=dubbo-provider&background=false&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=com.msb.common.service.IUserService&metadata-type=remote&methods=getUserById&pid=12752&register-mode=interface&release=3.1.8&side=provider&timeout=100&timestamp=1685542170287

我们看下 Zookeeper 的前后对比:

前:

image-20230531221438332

后:

image-20230531221506688

这时候拿出我们的UnCode转换器将乱码进行转换:

dubbo://192.168.0.103:20883/com.common.service.IUserService?anyhost=true&application=dubbo-provider&background=false&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=com.msb.common.service.IUserService&metadata-type=remote&methods=getUserById&pid=12752&register-mode=interface&release=3.1.8&side=provider&timeout=100&timestamp=1685542170287

发现其完全正确,这样我们的信息就被注册到了 Zookeeper 上。

3、疑惑解答

我们可以看到,我们第一次获取注册 ZookeeperURL 是:

这个 127.0.0.1:2181 是我们当前 Zookeeper 的地址,

service-discovery-registry://127.0.0.1:2181/org.apache.dubbo.registry.RegistryService?application=dubbo-provider&dubbo=2.0.2&metadata-type=remote&pid=13064&qos.enable=true&register-mode=instance&registry=zookeeper&release=3.1.8&timestamp=1685375979873

通过该 URL 我们可以和我们的 Zookeeper 进行一些相互

第二次拿到的 URL

这个 192.168.0.103:20883 是我们 Netty 服务器暴露的地址,将该地址注册至 Zookeeper,便于消费者的访问

dubbo://192.168.0.103:20883/com.common.service.IUserService?anyhost=true&application=dubbo-provider&background=false&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=com.msb.common.service.IUserService&metadata-type=remote&methods=getUserById&pid=12752&register-mode=interface&release=3.1.8&side=provider&timeout=100&timestamp=1685542170287

四、总结

鲁迅先生曾说:独行难,众行易,和志同道合的人一起进步。彼此毫无保留的分享经验,才是对抗互联网寒冬的最佳选择。

其实很多时候,并不是我们不够努力,很可能就是自己努力的方向不对,如果有一个人能稍微指点你一下,你真的可能会少走几年弯路。

如果你也对 后端架构和中间件源码 有兴趣,欢迎添加博主微信:hls1793929520,一起学习,一起成长

我是爱敲代码的小黄,独角兽企业的 Java 开发工程师,CSDN 博客专家,喜欢后端架构和中间件源码。

我们下期再见。

我从清晨走过,也拥抱夜晚的星辰,人生没有捷径,你我皆平凡,你好,陌生人,一起共勉。

往期文章推荐:

  • 不亏是阿里三面,ConcurrentHashMap多线程扩容机制被面试官装到了
  • 美团二面:聊聊ConcurrentHashMap的存储流程
  • 从源码全面解析Java 线程池的来龙去脉
  • 从源码全面解析LinkedBlockingQueue的来龙去脉
  • 从源码全面解析 ArrayBlockingQueue 的来龙去脉
  • 从源码全面解析ReentrantLock的来龙去脉
  • 阅读完synchronized和ReentrantLock的源码后,我竟发现其完全相似
  • 从源码全面解析 ThreadLocal 关键字的来龙去脉
  • 从源码全面解析 synchronized 关键字的来龙去脉
  • 阿里面试官让我讲讲volatile,我直接从HotSpot开始讲起,一套组合拳拿下面试

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

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

相关文章

SpringBoot配置 -- SpringBoot快速入门保姆级教程(二)

文章目录 前言二、SpringBoot配置1. 了解配置文件的3种格式2.yaml格式语法规则3.读取yaml数据的3种方式4.多环境开发配置5.多环境命令行启动参数设置6. 多环境开发兼容问题7.配置文件分类 总结 前言 为了巩固所学的知识&#xff0c;作者尝试着开始发布一些学习笔记类的博客&am…

vcruntime140.dll如何修复

VCRUNTIME140.dll是Windows操作系统上一个非常重要的动态链接库文件&#xff0c;它是由Microsoft Visual C Runtime提供的运行时库文件之一&#xff0c;被许多应用程序用来进行编译和运行。如果该文件丢失或损坏&#xff0c;很多应用程序就无法正常运行&#xff0c;这可能会带来…

三分钟了解SpringBoot配置优先级底层源码解析

&#x1f44f;作者简介&#xff1a;大家好&#xff0c;我是冰点&#xff0c;从业11年&#xff0c;目前在物流独角兽企业从事技术方面工作&#xff0c;&#x1f342;博主正在努力完成2023计划中&#xff1a;以梦为马&#xff0c;扬帆起航&#xff0c;2023追梦人&#x1f4dd;联系…

关于性能测试平台的一些想法,想跟大家聊一下

目录 一、任务管理 二、用例管理 三、环境管理 四、压测机管理 五、数据管理 六、监控管理 七、日志管理 八、报表管理 九、配置管理 十、系统管理 组织架构 这里我按照每个不同系统归属的项目组为横向&#xff0c;性能测试团队作为职能部门为纵向的矩阵式组织架构为…

JUC学习(二)

目录 Doug Lea — JUC并发包的作者锁框架Lock和Condition接口可重入锁公平锁与非公平锁读写锁锁降级和锁升级队列同步器AQS底层实现公平锁一定公平吗&#xff1f;Condition实现原理 ——————————————————————————————— 在前面&#xff0c;我们了解…

ICV报告:乘光伏新能源汽车之势,功率器件蓄势待发

前言&#xff1a; 电力电子器件&#xff08;Power Electronic Device&#xff09;&#xff0c;又称为功率半导体器件&#xff0c;用于电能变换和电能控制电路中的大功率(通常指电流为数十至数千安&#xff0c;电压为数百伏以上)电子器件。功率器件能够承受和控制较大电流、电压…

无限阳光、自动收集阳光CALL、阳光产生速度

简单实现无限阳光 本次实验内容&#xff1a;通过逆向分析植物阳光数量的动态地址找到阳光的基址与偏移&#xff0c;从而实现每次启动游戏都能够使用基址加偏移的方式定位阳光数据&#xff0c;最后我们将通过使用C语言编写通用辅助实现简单的无限阳光外挂&#xff0c;在教程开始…

Vue Router路由管理器

目录&#xff1a; 相关理解基本路由几个注意事项嵌套&#xff08;多级&#xff09;路由路由的query参数命名路由路由的params参数路由的props配置路由跳转的replace方法编程式路由导航缓存路由组件activated和deactivated路由守卫路由器的两种工作模式 相关理解 vue-route…

博学谷学习记录】超强总结,用心分享 | 架构师 敏捷开发 学习总结

文章目录 敏捷开发1. 概述2. 敏捷开发 敏捷开发 1. 概述 随着软件开发技术的不断发展&#xff0c;现在出现了很多种不同的开发模式&#xff0c;其实敏捷开发已经成为现在很多企业开发应用程序都想要选择的开发方案&#xff0c;那么什么是敏捷开发呢&#xff1f;1.1 四种开发模…

Linux 配置Java环境(一)

Linux 配置Java环境 一、配置Java环境1、查看系统是否有java环境2、卸载系统自带的jdk3、创建一个文件夹用于存放java的压缩包4、包下载好的jdk拖到java文件夹5、安装jdk6、配置环境变量7、让配置生效8、验证是否配置成功 一、配置Java环境 1、查看系统是否有java环境 输入指…

nginx中location和rewrite

常用的Nginx 正则表达式 ^ &#xff1a;匹配输入字符串的起始位置 $ &#xff1a;匹配输入字符串的结束位置 * &#xff1a;匹配前面的字符零次或多次。如“ol*”能匹配“o”及“ol”、“oll” &#xff1a;匹配前面的字符一次或多次。如“ol”能匹配“ol”及“oll”、“olll…

0-1背包问题:动态规划的经典应用

文章目录 引言背包问题简介0-1背包问题定义0-1背包问题的限制条件 动态规划解决思路状态定义状态转移方程 背包问题的Java实现示例与分析 总结 引言 背包问题是在给定一组物品和一个背包容量的情况下&#xff0c;如何选择物品放入背包&#xff0c;以使得放入背包的物品总价值最…

高边功率开关参数Load current(ISO)和Nominal current

1. IL(nom)是没有加散热片的情况下&#xff0c;考虑RON和BTS6133D与环境热阻计算得到的电流值&#xff1b; 2. IL(iso)是有散热片的情况下计算得到的电流值&#xff1b; 3. IL12(SC)是如果负载电流达到75A以上&#xff0c;BTS6143D会通过不断重启来来限制电流在75A以下。

【论文简述】GeoMVSNet: Learning Multi-View Stereo with Geometry Perception(CVPR 2023)

一、论文简述 1. 第一作者&#xff1a;Jie Zhu 2. 发表年份&#xff1a;2023 3. 发表期刊&#xff1a;CVPR 4. 关键词&#xff1a;MVS、级联结构、几何感知、频域增强、高斯混合模型 5. 探索动机&#xff1a;基于级联的结构以从粗到细的方式计算不同分辨率的深度图&#x…

数据结构与算法练习(二)数组模拟队列

文章目录 1、队列2、数组实现队列3、数组实现循环队列 1、队列 队列是一个有序列表&#xff0c;可以用数组或链表来实现 遵循先入先出的原则 在队尾插入元素叫做入队&#xff0c;对头删除元素叫做出队。2、数组实现队列 思路&#xff1a; 用front和rear记录队列前后的下标&a…

【MCS-51单片机汇编语言】期末复习总结⑤——定时器中断方式与查询方式程序设计(题型五)

文章目录 知识准备工作方式寄存器TMODTCON寄存器IE寄存器 定时/计数器的使用初始化 常考题型例题1题目描述题目解析题解 例题2题目描述题目解析题解 知识准备 工作方式寄存器TMOD D7D6D5D4D3D2D1D0TF1TR1TF0TR0IE1IT1IE0IT0 [注]&#xff1a;TMOD高4位与T1相关&#xff0c;低…

iptables和防火墙

文章目录 1.防火墙2.Iptables基本介绍2.1 什么是iptables2.2 什么是包过滤防火墙2.3 包过滤防火墙如何实现 1.防火墙 Linux防火墙主要工作在网络层&#xff0c;针对 TCP/IP 数据包实施过滤和限制&#xff0c;典型的包过滤防火墙&#xff0c;基于内核编码实现&#xff0c;具有非…

Qt(C++)绘制指针仪表盘显示当前温度

一、功能介绍 当前文章要实现的功能: 使用Qt绘制一个仪表盘,用来显示当前的温度,绘制刻度、绘制数字、绘制温度指针。仪表盘全程使用QPainter进行绘制,QPainter是Qt框架中非常重要的一个类,绘制功能的实现离不开它。如果想要使用Qt进行高质量的绘图或UI设计,必须掌握QP…

Groovy 基本语法

一、简介 类型转换:当需要时,类型之间会自动发生类型转换: 字符串&#xff08;String&#xff09;、基本类型(如int) 和类型的包装类(如Integer) 类说明&#xff1a;如果在一个groovy 文件中没有任何类定义&#xff0c;它将被当做script 来处理&#xff0c;也就意味着这个文件将…

DragGAN-点对点的手动P图

原文网站link以及论文链接&#xff1a; https://vcai.mpi-inf.mpg.de/projects/DragGAN/ https://arxiv.org/pdf/2305.10973.pdf 简介 深度生成模型近年在合成随机真实感图像方面取得了不错的成绩。在现实世界的应用中&#xff0c;这种基于学习的图像合成方法中一个关键功能是…