【Spring专题】Spring底层核心原理解析

目录

  • 前言
  • 阅读导航
  • 前置知识
    • Q1:你能描述一下JVM对象创建过程吗?
    • Q2:Spring的特性是什么?
    • 前置知识总结
  • 课程内容
    • 一、Spring容器的启动
    • 二、一般流程推测
      • 2.1 扫描
      • 2.2 IOC
      • 2.3 AOP
    • 2.4 小结
    • 三、【扫描】过程简单推测
    • 四、【IOC】过程简单推测
      • 4.1 推断构造方法过程细讲
    • 五、【AOP】过程简单推测
    • 六、Spring事务
  • 学习总结

前言

Spring啊,可以说是我们大部分Java玩家【最熟悉的陌生人】了吧。八个字形容:似懂非懂,会也不会
在这里插入图片描述
你说简单应用,我们大家都会,那真要展开说两句的话,那只能来这么两句:这是第一句,接着是第二句,好了我说完了。
在这里插入图片描述
但是啊xdm,据说Spring是一份非常非常非常优秀的源码,不但有丰富的设计模式应用场景,代码写的也很优美,有条理,所以非常推荐大家学习。除了能在日常装逼以外,还能丰富一下见识,提升自己写代码的能力。

阅读导航

阅读对象:有过Spring开发经验的人

前置知识

Q1:你能描述一下JVM对象创建过程吗?

答:看图说话:
在这里插入图片描述

  1. 类加载:在使用一个类之前,Java虚拟机需要先将类的字节码加载到内存中。类加载是Java虚拟机的核心过程,它负责查找类的字节码文件并加载到内存的方法区。类加载包括加载、验证、准备、解析和初始化这五个阶段。
  2. 分配内存:在类加载完成后,Java虚拟机会为对象分配内存空间。内存分配通常在堆(Heap)上进行,但也有一些特殊情况下的对象可以在栈(Stack)上分配内存,例如线程栈上的局部对象。
  3. 实例化(初始化零值):在分配内存后,Java虚拟机会将对象的内存空间初始化为零值。这包括基本类型的默认值(例如0、false等)和引用类型的默认值(null)。
  4. 设置对象头:Java对象在内存中的布局包括对象头和实例数据两部分。对象头存储了一些元数据,如对象的哈希码、锁状态等。在对象创建过程中,Java虚拟机会设置对象头的值。
  5. 执行构造方法:对象创建的最后一步是执行构造方法。构造方法用于初始化对象的实例数据,并执行其他必要的初始化操作。构造方法可以是类的默认构造方法,也可以是自定义的构造方法。
  6. 返回对象引用:对象创建完成后,Java虚拟机会返回一个指向该对象的引用。通过引用,程序可以操作对象的属性和方法。

PS:为什么要问这个问题?因为Spring是IOC技术,就算再怎么玩出花来,他也要按照这个基本流程来创建对象。只不过,可以提前告知大家的是,SpringIOC在这个流程之中,新增了很多槽点,通过热插拔的方式,丰富了IOC的功能!

Q2:Spring的特性是什么?

答:Spring的特性就是IOC跟AOP两大概念!甚至可以这么说:Spring就是实现了AOP技术的IOC容器(容器,容器,容器)

Q3:什么是IOC,什么是AOP?
答:下面答案来源于百度【文心一言】:

  • IOC(控制反转)是一种设计模式(思想),它允许将对象的创建和管理交给Spring容器来处理,而不是在代码中直接创建对象。通过使用IOC,可以将对象的依赖关系从代码中解耦,使得代码更加灵活、可维护和可测试。
  • AOP(面向切面编程)也是一种设计模式(思想),它通过预编译方式和运行期动态代理实现在不修改源代码的情况下给程序动态添加功能。AOP解决了面向对象编程中无法解决的问题,例如事务管理、安全性、日志记录等。

Spring框架通过实现IOC和AOP,使得程序更加模块化、灵活和易于维护。同时,Spring还提供了许多其他模块和功能,如DAO、ORM、WebMVC等,使得它成为一个功能强大的Java开发框架。

前置知识总结

从上面的问题里面,我们提到了一个很重要的东西,即:Spring就是实现了AOP技术的IOC容器。并且,也概括地描述了IOC跟AOP的概念。既然我们也知道了,IOC其实也管理了对象的创建,那么说到对象创建,肯定也离不开我们在Q1说的,对象创建的过程。而且,无论对象怎么创建,谁创建,都没办法离开上面的流程的。
事实上,可以提前告诉大家的是,IOC在创建对象的过程,无非就是在上面的对象创建流程中,丰富了一些细节,新增了一些拓展点,为Spring功能实现提供支持。

课程内容

为了展开对Spring源码的研究,我们这里线大致地串讲一下Spring的一些核心知识点,让大家对Spring的底层一些基础逻辑有个清晰的认知。

一、Spring容器的启动

我想,经历过SSM/SSH时代的朋友,对下面的代码都不会陌生:

ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");
UserService userService = (UserService) context.getBean("userService");
userService.test();
System.out.println(userService);
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-4.3.xsd">

<!--    <import resource="引入其他bean xml配置文件" />-->
    <bean id="userService" class="org.example.spring.bean.UserService"/>
</beans>

如果真的很陌生也没关系,下面这个可能就相对熟悉一点了:(后面也会围绕这个启动方式的Spring讲解。除了是下面的比较主流,也因为,下面这种方式使用更广、更新,内容相对丰富点!

AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
UserService userService = (UserService) context.getBean("userService");
userService.test();
System.out.println(userService);
@Component
public class UserService {

    public void test() {
        System.out.println("这是一个测试方法");
    }
}

哈哈,我估计很多直接进入了Java【SpringBoot】时代的朋友,可能连上面这个都没看到过。
那上面段代码是干啥的呢?很简单,就是启动一个Spring容器而已。上面两个不同的启动方式,也仅仅是Bean注册方式不一样。比如前者是通过读取xml里面的<bean>标签定义,后者是读取的注解式Bean。
到这里,想问大家一个问题,那就是,通过上面第二种方式的代码,你发现了什么?我的发现是:我仅仅只是调用了一行代码,就可以开始使用Spring定义的Bean了,什么依赖注入,AOP啥的,我都没管,直接就可以了。这证明了啥?其实很粗浅,也有点废话,那就是证明:通过这一行代码,里面就帮我完成了所有我们平时使用过的,Spring的基础能力

二、一般流程推测

根据我们之前学习过Spring相关的操作,简单推测一下,这一行代码里面干了什么。

AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);

2.1 扫描

首先是第一点【扫描】。我们在项目中,写了这么多Bean,或者说,我项目里面这么多类,哪些是Bean,哪些是普通类,Spring是怎么识别到的?其实道理很简单的,Spring它也没那么智能,想要获取这个类的信息,Spring肯定要【亲自】去看一眼,才能知道这个类的具体信息。你有这么多少个文件,他就要扫描多少个类。关键代码如下:

// 定义需要扫描的基础包名
@ComponentScan("org.tuling.spring")
public class AppConfig {
}

2.2 IOC

扫描完了所有的文件,那基本上Spring已经能确定,哪些是Bean,哪些是普通类了。接下来,就可以开始创建Bean了,这里,就是所谓的IOC过程

2.3 AOP

AOP肯定是发生在IOC之后的,如果你们了解设计模式里面的【代理模式】的话,理解这一点并不难。毕竟,如果目标对象功能不完整,代理对象的功能也会收到影响。

2.4 小结

在这里插入图片描述

三、【扫描】过程简单推测

之前我们也说了,扫描,就是需要Spring亲自去看一看,哪些是需要被创建Bean的,哪些是不需要的。就拿我们举例用的new AnnotationConfigApplicationContext(AppConfig.class)来说,大概步骤如下:(简单推测,不详)

  1. 它需要先看AppConfig.class,读取扫描包的基础路径
  2. 根据上一步读到的基础路径,遍历包下所有的文件,如果类上存在@Component@Service等注解,则确认为是一个Bean
  3. 筛选完后将读取的Bean信息记录下来,比如说存到一个Map里面,方便后续遍历

在这里插入图片描述

四、【IOC】过程简单推测

IOC过程,其实在Spring中有个比较专业的术语,叫做:Bean的生命周期。简单的几个字,包含了很多内容。在此之前,大家先看一看【前置知识】里面【JVM对象创建过程】,加深一下印象。
其中有:

  1. 新建一个Bean对象。说到新建Bean对象,无论如何都绕不开构造方法的调用。我有多个构造函数,那我应该使用哪个呢?这在Spring内部,叫做:【推断构造方法】;
  2. Bean对象创建好了之后,当然不能忘了我们的:【依赖注入】;
  3. 有一点大家可能用的会比较少,但如果有过经验的同学可能会比较熟悉,比如:ApplicationContextAware,我们可以直接直译,叫做:ApplicationContext感知,感知ApplicationContext,所以通过设置,我们就可以在Bean里面获取到这个组件了;
  4. init方法。如果大家有过@PostConstruct以及InitializingBean使用经验的话,或许知道,创建完Bean之后,在返回Bean之前,还有这一步动作。事实上,根据我们在前言里面说的【IOC在创建对象的过程,无非就是在上面的对象创建流程中,丰富了一些细节,新增了一些拓展点】,所以,在IOC里面还有很多切入点,比如:实例化前、实例化后;初始化前、初始化后等等
  5. 最后,就是进入AOP的流程了。如果需要AOP,则进行AOP,并且返回AOP对象;否则直接返回已经创建好的对象

另外需要注意的是,Bean对象创建出来后:

  1. 如果当前Bean是单例Bean,那么会把该Bean对象存入一个Map<String, Object>,Map的key为beanName,value为Bean对象。这样下次getBean时就可以直接从Map中拿到对应的Bean对象了(实际上,在Spring源码中,这个Map就是单例池);
  2. 如果当前Bean是原型Bean,那么后续没有其他动作,不会存入一个Map,下次getBean时会再次执行上述创建过程,得到一个新的Bean对象。

在这里插入图片描述

4.1 推断构造方法过程细讲

Spring在基于某个类生成Bean的过程中,需要利用该类的构造方法来实例化得到一个对象,但是如果一个类存在多个构造方法,Spring会使用哪个呢?

Spring的判断逻辑如下:

  1. 如果一个类只有一个构造函数,不管构造函数是有参,还是无参,Spring都会使用这个构造函数创建对象,因为没得选了;
  2. 如果这个类存在多个构造函数:
    • 如果存在无参构造函数,则使用无参构造函数。因为在Java里面,无参构造函数本身就具有默认的意思在里面;
    • 如果没有无参构造函数,则看多个无参构造函数,哪个有@Autowired修饰,有就选择;没有就只能报错了

还有一个问题。如果Spring选择了一个有参的构造方法,Spring在调用这个有参构造方法时,需要传入参数,那这个参数是怎么来的呢?答案是:Spring会根据入参的类型和入参的名字去Spring中找Bean对象。
3. 先根据入参类型找,如果只找到一个,那就直接用来作为入参;
4. 如果根据类型找到多个,则再根据入参名字来确定唯一一个;
5. 最终如果没有找到,则会报错,无法创建当前Bean对象。

五、【AOP】过程简单推测

AOP就是进行动态代理,在创建一个Bean的过程中,Spring在最后一步(放入单例池之前)会去判断当前正在创建的这个Bean是不是需要进行AOP,如果需要则会进行动态代理。
那么,如何判断一个Bean是否需要被AOP代理呢?步骤如下:

  1. 找出所有的切面Bean(切面也是Bean来的,或者叫做:特殊的Bean)
  2. 遍历切面中的每个方法,看是否写了@Before、@After等注解(通知)
  3. 如果写了,则判断所对应的Pointcut是否和当前Bean对象的类是否匹配
  4. 如果匹配则表示当前Bean对象有匹配的的Pointcut,表示需要进行AOP

利用cglib进行AOP的大致流程:(看上面的代理范式大概就知道了)

  1. 新增一个代理类XxxProxy,继承自被代理对象XxxTarget,并且持有一个XxxTarget成员变量(这个成员变量需要经过一个Bean的声明周期,即,完成了IOC等)
  2. 在代理类中重写父类的方法
  3. 执行代理类的方法时,调用的代理类的方法,但同时也需要执行切面的逻辑

在这里插入图片描述

然后这里给大家一个【代理模式】的范式:

// 被代理对象
public class ProxyTarget {
    public void run() {
        System.out.println("这是普通对象的run");
    }
}


// 代理对象
public class ProxyModel extends ProxyTarget {
    private ProxyTarget proxyTarget;

    public void setProxyTarget(ProxyTarget proxyTarget) {
        this.proxyTarget = proxyTarget;
    }

    @Override
    public void run() {
        System.out.println("我代理对象可以在这里做加强---1");
        super.run();
        System.out.println("我代理对象也可以在这里做加强---2");
    }
}

六、Spring事务

当我们在某个方法上加了@Transactional注解后,就表示该方法在调用时会开启Spring事务,而这个方法所在的类所对应的Bean对象会是该类的代理对象。
Spring事务的代理对象执行某个方法时的步骤:

  1. 判断当前执行的方法是否存在@Transactional注解
  2. 如果存在,则利用事务管理器(TransactionMananger)新建一个数据库连接
  3. 修改数据库连接的autocommit为false
  4. 执行target.test(),执行程序员所写的业务逻辑代码,也就是执行sql
  5. 执行完了之后如果没有出现异常,则提交,否则回滚

Spring事务是否会失效的判断标准:某个加了@Transactional注解的方法被调用时,要判断到底是不是直接被代理对象调用的,如果是则事务会生效,如果不是则失效。(PS:这一点很容易被疏忽)
另外,还有个经典例子,那就是@Bean在有跟没有@Configuration的时候,结果是不一样的,如下:

声明Bean的方法:

@ComponentScan("org.tuling.spring")
@Configuration
public class AppConfig {
    
    @Bean
    public UserService userService() {
        return new UserService(walletService());
    }

    @Bean
    public UserService userService1() {
        return new UserService(walletService());
    }

    @Bean
    public WalletService walletService() {
        return new WalletService();
    }
}

// UserService声明
public class UserService {
    private WalletService walletService;

    public UserService() {}
    public UserService(WalletService walletService) {
        this.walletService = walletService;
    }

    public WalletService getWalletService() {
        return walletService;
    }

    /**
     * 自我介绍
     */
    public void selfIntroduction() {
        System.out.println("你好,我是阿通,我有好多钱");
        walletService.showMyBalance();
    }
}

大家看上面的声明Bean的方法,按照设想,WalletService肯定也是一个单例嘛,所以,userServiceuserService1持有的walletService对象肯定是一样的。

调用方法:

        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);

        UserService userService = (UserService)context.getBean("userService");
        System.out.println(userService);
        System.out.println(userService.getWalletService());


        System.out.println("--------------------------------");
        UserService userService1 = (UserService)context.getBean("userService1");
        System.out.println(userService1);
        System.out.println(userService1.getWalletService());

结果输出如下:

org.tuling.spring.bean.UserService@2c34f934
org.tuling.spring.bean.WalletService@12d3a4e9
--------------------------------
org.tuling.spring.bean.UserService@240237d2
org.tuling.spring.bean.WalletService@12d3a4e9

看结果,没什么问题,如期输出。但如果我们把声明Bean的方法里面的@Configuration去掉,结果会变成这样:

@ComponentScan("org.tuling.spring")
//@Configuration
public class AppConfig {

    @Bean
    public UserService userService() {
        return new UserService(walletService());
    }

    @Bean
    public UserService userService1() {
        return new UserService(walletService());
    }

    @Bean
    public WalletService walletService() {
        return new WalletService();
    }
}
org.tuling.spring.bean.UserService@710726a3
org.tuling.spring.bean.WalletService@646007f4
--------------------------------
org.tuling.spring.bean.UserService@481a15ff
org.tuling.spring.bean.WalletService@78186a70

为什么,只是简单注释了一个@Configuration结果就不一样了呢?分析如下:

  1. @Bean注解可以将方法返回的对象注册为一个 Bean,并且该 Bean 会被 Spring 容器管理。仅此而已
  2. 所以,在userService()方法重调用walletService()方法,实际上就是一个普通Java调用而已,肯定会重新new WalletService()
  3. 而被@Configuration注解之后,所有方法都将被代理(暂时还没找到源码证据,等后面我看懂了再附上)

学习总结

  1. 简单学习了Spring启动的流程
  2. 通过一些常见Spring操作的串讲,大概了解了一下IOC和AOP的大致流程

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

install imap error

【错误翻译】 Try to run this command from the system terminal. Make sure that you use the correct version of pip installed for your Python interpreter located at D:\Program Files (x86)\Python\Python39\python.exe. 尝试从系统终端运行此命令。请确保使用安装在…

竞赛项目 疫情数据分析与3D可视化 - python 大数据

文章目录 0 前言1 课题背景2 实现效果3 设计原理4 部分代码5 最后 0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; 大数据全国疫情数据分析与3D可视化 该项目较为新颖&#xff0c;适合作为竞赛课题方向&#xff0c;学长非常推荐&#xff0…

Vue+SpringBoot项目开发:后台登陆功能的实现(二)

写在开始:一个搬砖程序员的随缘记录文章目录 一、SpringBoot项目的搭建二、数据库配置1、新建数据库2、新建用户表 三、SpringBoot项目的配置 一、SpringBoot项目的搭建 项目搭建传送门&#xff1a;从零开始&#xff0c;SpringBoot项目快速搭建 二、数据库配置 1、新建数据库…

nacos安装与启动相关问题(启动闪退和显示此站点的连接不安全)

问题&#xff1a;启动闪退 尝试&#xff1a; 使用记事本打开cmd文件&#xff0c;在文件结尾处新增两行 pause endlocal 如果还有问题&#xff1a;ERROR Nacos failed to start, please see D:\dev\nacos\logs\nacos.log for more details 尝试&#xff1a; 在nacos的bin目…

【100天精通python】Day30:使用python操作数据库_数据库基础入门

专栏导读 专栏订阅地址&#xff1a;https://blog.csdn.net/qq_35831906/category_12375510.html 1 数据库基础知识介绍 1.1 什么是数据库&#xff1f; 数据库是一个结构化存储和组织数据的集合&#xff0c;它可以被有效地访问、管理和更新。数据库的目的是为了提供一种可靠的…

Python-OpenCV中的图像处理-边缘检测

Python-OpenCV中的图像处理-边缘检测 边缘检测Canny算子 边缘检测Canny算子 Canny 边缘检测是一种非常流行的边缘检测算法&#xff0c;是 John F.Canny 在 1986 年提出的。它是一个有很多步构成的算法&#xff1a;噪声去除、计算图像梯度、非极大值抑制、滞后阀值等。 Canny(i…

Android:换肤框架Android-Skin-Support

gihub地址&#xff1a;https://github.com/ximsfei/Android-skin-support 样例&#xff1a; 默认&#xff1a; 更换后&#xff1a; 一、引入依赖&#xff1a; // -- 换肤依赖implementation skin.support:skin-support:4.0.5// skin-supportimplementation skin.support:ski…

Linux系统的Centos7扩容主分区

前言&#xff1a;在学习C#的过程中电脑里面的项目&#xff0c;镜像越来越多之前装系统的时候分配的空间太小导致Linux系统空间不足&#xff0c;应该怎么办呢&#xff0c;lets go 跟着我来将centOS 7扩容吧. 1.关闭虚拟机&#xff0c;在VMWare的”此虚拟机设置“中找到硬盘&…

半年报增幅超预期,但汤臣倍健还是喂不饱年轻人?

文|琥珀消研社 作者|朱古力 谁也想不到&#xff0c;进入三伏天以后首个运动潮流会是“晒背”。 小红书上近5万条“晒背”相关的笔记里&#xff0c;多数都是对“什么时候晒背”、“晒多久合适”等入门级问题的答疑解惑&#xff0c;微博热搜词条更是针对性地给出了“晒背最佳时…

19 | 首尔自行车共享需求预测

文章目录 首尔自行车共享需求预测1. 问题陈述2. 数据描述3. 项目的业务用途是什么?4. 模型构建步骤5. 数据可视化1)热力图2)按季节分的租赁自行车数量3)假日和非假日租赁自行车数量4)太阳辐射与租赁自行车数量5)租赁自行车数量和一天的小时数6)特征重要性首尔自行车共享…

如何选择适合自己的文件传输工具

随着互联网的发展&#xff0c;人们处理文件的需求也随之增加。不管是工作还是生活中&#xff0c;文件传输都是一个非常常见的问题。因此&#xff0c;如何选择适合自己的文件传输工具也越来越重要。在本文中&#xff0c;我将从以下几个方面进行分析和总结&#xff0c;希望能为大…

应用案例|基于三维机器视觉的机器人纸箱拆码垛应用解决方案

Part.1 项目背景 在现代物流和制造行业中&#xff0c;纸箱的拆码垛操作是一项重要且频繁的任务。传统的纸箱拆码垛工作通常由人工完成&#xff0c;这种方式存在劳动强度大、生产效率低以及人为操作容易导致错误等问题&#xff0c;严重影响物料的安全运输和质量。为了满足物流行…

数据结构刷题训练——链表篇(二)

目录 前言 1.题目一&#xff1a;链表分割 1.1 思路 1.2 分析 1.3 题解 2. 题目二&#xff1a;相交链表 2.1 思路 2.2 分析 2.3 题解 3. 题目三&#xff1a;环形链表 3.1 思路 3.2 分析 3.3 题解 总结 前言 本期继续分享链表相关的OJ题目&#xff0c;在这个专栏博客…

《Linux从练气到飞升》No.11 初识操作系统

&#x1f57a;作者&#xff1a; 主页 我的专栏C语言从0到1探秘C数据结构从0到1探秘Linux菜鸟刷题集 &#x1f618;欢迎关注&#xff1a;&#x1f44d;点赞&#x1f64c;收藏✍️留言 &#x1f3c7;码字不易&#xff0c;你的&#x1f44d;点赞&#x1f64c;收藏❤️关注对我真的…

Python实现调用百度翻译的API

import requests import hashlib import random import jsondef translate(text, from_lang, to_lang):app_id XXXXX # 替换为你的App IDsecret_key XXXXX # 替换为你的Secret Key# 生成随机数salt random.randint(32768, 65536)# 计算签名sign app_id text str(salt) …

vue的proxy代理详解

一、proxy常用参数说明 module.exports {publicPath: "/",devServer: {proxy: {"/api": {// 代理名称 凡是使用/api开头的地址都是用此代理target: "http://1.2.3.4:5000/", // 需要代理访问的api地址changeOrigin: true, // 允许跨域请求pa…

【云原生】K8S集群

目录 一、调度约束1.1 POT的创建过程1.1调度过程 二、指定节点调度2.1 通过标签选择节点 三、亲和性3.1requiredDuringSchedulingIgnoredDuringExecution&#xff1a;硬策略3.1 preferredDuringSchedulingIgnoredDuringExecution&#xff1a;软策略3.3Pod亲和性与反亲和性3.4使…

---------------- 部署 Zookeeper 集群 ----------------

部署 Zookeeper 集群 1.安装前准备2.安装 Zookeeper修改配置文件在每个节点上创建数据目录和日志目录在每个节点的dataDir指定的目录下创建一个 myid 的文件配置 Zookeeper 启动脚本 //准备 3 台服务器做 Zookeeper 集群 192.168.109.1 192.168.109.2 192.168.109.3 1.安装前准…

Zorin OS 16.3 发布:无缝升级和卓越改进!

导读Zorin OS 团队自豪地宣布了备受期待的 Zorin OS 16.3 版本的发布&#xff0c;这是这个受欢迎的 Linux 发行版的一个里程碑版本。自首次发布以来不到两年时间&#xff0c;Zorin OS 已经获得了庞大的用户群体&#xff0c;截至目前已经有 530 万次下载&#xff0c;而 16.3 版本…

QGIS二次开发六:VS不借助QT插件创建UI界面

上一篇博客我们说了在VS中如何使用QT插件来创建UI界面&#xff0c;但是我们二次开发QGIS的第一篇博客就说了&#xff0c;最好使用OSGeo4W中自动下载的QT进行QGIS二次开发&#xff0c;这样兼容性是最好的&#xff0c;那么该如何在VS中不使用外部安装的QT以及QT的VS插件情况下进行…