Spring系列-SpringMvc父子容器启动原理解析

news2025/1/16 15:51:27

1、Spring整合SpringMVC

特性:

说到Spring整合SpringMVC唯一的体现就是父子容器:

  • 通常我们会设置父容器(Spring)管理Service、Dao层的Bean, 子容器(SpringMVC)管理Controller的Bean .
  • 子容器可以访问父容器的Bean, 父容器无法访问子容器的Bean。

实现:

相信大家在SSM框架整合的时候都曾在web.xml配置过这段:

<!--spring 基于web应用的启动-->
<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!--全局参数:spring配置文件-->
<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:spring-core.xml</param-value>
</context-param>
<!--前端调度器servlet-->
<servlet>
    <servlet-name>dispatcherServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <!--设置配置文件的路径-->
    <init-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:spring-mvc.xml</param-value>
</init-param>
    <!--设置启动即加载-->
    <load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
    <servlet-name>dispatcherServlet</servlet-name>
    <url-pattern>/</url-pattern>

但是它的作用是什么知道吗?

0

有人可能只知道DispatcherServlet叫前端控制器,是SpringMVC处理前端请求的一个核心调度器

那它为什么能处理请求?处理之前做了什么准备工作呢?又是怎么和Spring结合起来的呢?

为什么有了DispatcherServlet还要个ContextLoaderListener, 配一个不行吗?干嘛要配俩啊?

看完本文你就会有答案!

0

还有人可能会觉得, 我现在都用SpringBoot开发, 哪还要配这玩意.......

0

这就是典型的SpringBoot使用后遗症,SpringBoot降低了使用难度,但是从某种程度来说,也让初级的程序员变得更加小白,把实现原理都隐藏起来了而我们只管用,一旦涉及扩展就束手无策。

那当然我们今天不讲SpringBoot,我们今天用贴近SpringBoot的方式来讲SpringMVC。也就是零配置(零xml)的放式来说明SpringMVC的原理!!

此方式作为我们本文重点介绍,也是很多人缺失的一种方式, 其实早在Spring3+就已经提供, 只不过我们直到SpringBoot才使用该方式进行自动配置, 这也是很多人从xml调到SpringBoot不适应的原因, 因为你缺失了这个版本。 所以我们以这种方式作为源码切入点既可以理解到XML的方式又能兼顾到SpringBoot的方式 。

2、零配置SpringMVC实现方式:

那没有配置就需要省略掉web.xml 怎么省略呢?

在Servlet3.0提供的规范文档中可以找到2种方式:

  • 注解的方式
  1. @WebServlet
  2. @WebFilter
  3. @WebListener

但是这种方式不利于扩展, 并且如果编写在jar包中tomcat是无法感知到的。

  • SPI的方式

在Serlvet3-1的规范手册中:就提供了一种更加易于扩展可用于共享库可插拔的一种方式,参见8.2.4:

也就是让你在应用META-INF/services 路径下 放一个 javax.servlet.ServletContainerInitailizer ——即SPI规范

SPI 我们叫他服务接口扩展,(Service Provider Interface) 直译服务提供商接口, 不要被这个名字唬到了, 其实很好理解的一个东西:

其实就是根据Servlet厂商(服务提供商)提供要求的一个接口, 在固定的目录(META-INF/services)放上以接口全类名 为命名的文件, 文件中放入接口的实现的全类名,该类由我们自己实现,按照这种约定的方式(即SPI规范),服务提供商会调用文件中实现类的方法, 从而完成扩展。

ok 那我们知道了SPI是什么,我们是不是可以在Web应用中,在Servlet的SPI放入对应的接口文件:

0

放入实现类:

0

通过ServletContext就可以动态注册三大组件:以Servlet注册为例:

public class TulingSpringServletContainerInitializer extends SpringServletContainerInitializer {

    @Override
    public void onStartup(Set<Class<?>> webAppInitializerClasses, ServletContext servletContext) throws ServletException {

        // 通过servletContext动态添加Servlet
        servletContext.addServlet("spiServlet", new HttpServlet() {
            @Override
            protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
                resp.getWriter().write("spiServlet--doGet");
            }
        }).addMapping("/spiServlet.do");


    }

 当然在SpringMVC中, 这个接口文件和实现类都把我们实现好了,甚至ContextLoaderListener和DispatcherServlet都帮我们注册好了,我们只要让他生效,来,看看他是怎么做的:

3、实现基于SPI规范的SpringMVC

TulingStarterInitializer

  • 此类继承AbstractAnnotationConfigDispatcherServletInitializer 这是个啥? 待会我们讲原理来介绍
  • getRootConfigClasses 提供父容器的配置类
  • getServletConfigClasses 提供子容器的配置类
  • getServletMappings 设置DispatcherServlet的映射
public  class TulingStarterInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

   /**
    * 方法实现说明:IOC 父容器的启动类
    * @author:xsls
    * @date:2019/7/31 22:12
    */
   @Override
   protected Class<?>[] getRootConfigClasses() {
      return new Class[]{RootConfig.class};
   }

   /**
    * 方法实现说明 IOC子容器配置 web容器配置
    * @author:xsls
    * @date:2019/7/31 22:12
    */
   @Override
   protected Class<?>[] getServletConfigClasses() {
      return new Class[]{WebAppConfig.class};
   }

   /**
    * 方法实现说明
    * @author:xsls
    * @return: 我们前端控制器DispatcherServlet的拦截路径
    * @exception:
    * @date:2019/7/31 22:16
    */
   @Override
   protected String[] getServletMappings() {
      return new String[]{"/"};

RootConfig

  • 父容器的配置类 =以前的spring.xml
  • 扫描的包排除掉@Controller
@Configuration
@ComponentScan(basePackages = "com.tuling",excludeFilters = {
      @ComponentScan.Filter(type = FilterType.ANNOTATION,value={RestController.class,Controller.class}),
      @ComponentScan.Filter(type = ASSIGNABLE_TYPE,value =WebAppConfig.class ),
})
public class RootConfig {

}

WebAppConfig

  • 子容器的配置类 =以前的spring-mvc.xml
  • 扫描的包:包含掉@Controller
@Configuration
@ComponentScan(basePackages = {"com.tuling"},includeFilters = {
      @ComponentScan.Filter(type = FilterType.ANNOTATION,value = {RestController.class, Controller.class})
},useDefaultFilters =false)
@EnableWebMvc   // ≈<mvc:annotation-driven/>
public class WebAppConfig implements WebMvcConfigurer{

   /**
    * 配置拦截器
    * @return
    */
   @Bean
   public TulingInterceptor tulingInterceptor() {
      return new TulingInterceptor();
   }

   /**
    * 文件上传下载的组件
    * @return
    */
   @Bean
   public MultipartResolver multipartResolver() {
      CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver();
      multipartResolver.setDefaultEncoding("UTF-8");
      multipartResolver.setMaxUploadSize(1024*1024*10);
      return multipartResolver;
   }

   /**
    * 注册处理国际化资源的组件
    * @return
    */
/* @Bean
   public AcceptHeaderLocaleResolver localeResolver() {
      AcceptHeaderLocaleResolver acceptHeaderLocaleResolver = new AcceptHeaderLocaleResolver();
      return acceptHeaderLocaleResolver;
   }*/

   @Override
   public void addInterceptors(InterceptorRegistry registry) {
      registry.addInterceptor(tulingInterceptor()).addPathPatterns("/*");
   }


   /**
    * 方法实现说明:配置试图解析器
    * @author:xsls
    * @exception:
    * @date:2019/8/6 16:23
    */
   @Bean
   public InternalResourceViewResolver internalResourceViewResolver() {
      InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
      viewResolver.setSuffix(".jsp");
      viewResolver.setPrefix("/WEB-INF/jsp/");
      return viewResolver;
   }



   @Override
   public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
      converters.add(new MappingJackson2HttpMessageConverter());
   }

自己去添加个Controller进行测试

OK, 现在可以访问你的SpringMVC了

4、SPI的方式SpringMVC启动原理

接着我们来看看SPI方式的原理是什么:

SpringMVC 大致可以分为 启动 和请求 2大部分, 所以我们本文先研究启动部分

流程图:

源码流程

  1. 外置Tomcat启动的时候通过SPI 找到我们应用中的/META-INF/service/javax.servlet.ServletContainerInitializer

0

  1. 调用SpringServletContainerInitializer.onStartUp()

0

    1. 调用onStartUp()前会先找到@HandlesTypes(WebApplicationInitializer.class) 所有实现了WebApplicationInitializer的类,传入到OnStartup的webAppInitializerClasses参数中,并传入Servlet上下文对象。
    2. 重点关注这组类:他们组成了父子容器

0

  1. 找到所有WebApplicationInitializer的实现类后, 不是接口、不是抽象则通过反射进行实例化(所以,你会发现内部实现类都是抽象的,你想让其起作用我们必须添加一个自定义实现类,在下文提供我的自定义实现类)
  2. 调用所有上一步实例化后的对象的onStartup方法

0

0

1. 首先来到AbstractDispatcherServletInitializer#onStartup再执行super.onStartup(servletContext);

@Override
public void onStartup(ServletContext servletContext) throws ServletException {
   //实例化我们的spring root上下文
   super.onStartup(servletContext);
   //注册我们的DispatcherServlet   创建我们spring web 上下文对象
   registerDispatcherServlet(servletContext);

 创建父容器——ContextLoaderListener

2.父类AbstractContextLoaderInitializer#onStartup执行registerContextLoaderListener(servletContext);

  1. createRootApplicationContext()该方法中会创建父容器
    1. 该方法是抽象方法,实现类是AbstractAnnotationConfigDispatcherServletInitializer
      1. 调用getRootConfigClasses();方法获取父容器配置类(此抽象方法在我们自定义的子类中实现提供我们自定义的映射路径 )
      2. 创建父容器,注册配置类

0

  1. 会创建ContextLoaderListener并通过ServletContext注册

0

看完大家是不是感觉跟我们XML的配置ContextLoaderListener对上了:

0

创建子容器——DispatcherServlet

3.回到AbstractDispatcherServletInitializer#onStartup再执行registerDispatcherServlet(servletContext);

0

registerDispatcherServlet方法说明:

  1. 调用createServletApplicationContext创建子容器
    1. 该方法是抽象方法,实现类是AbstractAnnotationConfigDispatcherServletInitializer
      1. 创建子容器(下图很明显不多介绍)
      2. 调用抽象方法:getServletConfigClasses();获得配置类(此抽象方法在我们自定义的子类中实现提供我们自定义的配置类 )
      3. 配置类除了可以通过ApplicationContext()构造函数的方式传入 , 也可以通过这种方式动态添加,不知道了吧~

0

  1. 调用createDispatcherServlet(servletAppContext);创建DispatcherServlet
  2. 设置启动时加载:registration.setLoadOnStartup(1);
  3. 调用抽象方法设置映射路径:getServletMappings()(此抽象方法在我们自定义的子类中实现提供我们自定义的映射路径 )

看完大家是不是感觉跟我们XML的配置DispatcherServlet对上了

0

4. 初始化ContextLoaderListener

0

ContextLoaderListener加载过程比较简单:

外置tomcat会帮我们调用ContextLoaderListener#contextInitialized 进行初始化

  1. xml的方式下会判断容器为空时创建父容器
  2. 在里面会调用父容器的refresh方法加载
  3. 将父容器存入到Servlet域中供子容器使用

0

5. 初始化DispatcherServlet

0

可以看到流程比ContextLoaderListener流程更多

外置tomcat会帮我们调用DispatcherServlet#init()   进行初始化--->重点关注:initWebApplicationContext方法

  1. getWebApplicationContext(getServletContext())获得父容器(从之前的Servlet域中拿到)
  2. cwac.setParent(rootContext);给子容器设置父容器
  3. 调用configureAndRefreshWebApplicationContext(cwac);

0

    1. 注册一个监听器(该监听会初始化springmvc所需信息)
      1. ContextRefreshedEvent可以看到该监听器监听的是容器refreshed事件, 会在finishRefresh中发布
    2. 刷新容器

0

当执行refresh 即加载ioc容器 完了会调用finishRefresh():

  1. publishEvent(new ContextRefreshedEvent(this));发布ContextRefreshedEvent事件
  2. 触发上面的ContextRefreshListener监听器:

---->FrameworkServlet.this.onApplicationEvent(event);

-------->onRefresh(event.getApplicationContext());

-------------->initStrategies(context);

protected void initStrategies(ApplicationContext context) {
   //初始化我们web上下文对象的 用于文件上传下载的解析器对象
   initMultipartResolver(context);
   //初始化我们web上下文对象用于处理国际化资源的
   initLocaleResolver(context);
   //主题解析器对象初始化
   initThemeResolver(context);
   //初始化我们的HandlerMapping
   initHandlerMappings(context);
   //实例化我们的HandlerAdapters
   initHandlerAdapters(context);
   //实例化我们处理器异常解析器对象
   initHandlerExceptionResolvers(context);
   initRequestToViewNameTranslator(context);
   //给DispatcherSerlvet的ViewResolvers处理器
   initViewResolvers(context);
   initFlashMapManager(context);

这里面的每一个方法不用太细看, 就是给SpringMVC准备初始化的数据, 为后续SpringMVC处理请求做准备

基本都是从容器中拿到已经配置的Bean(RequestMappingHandlerMapping、RequestMappingHandlerAdapter、HandlerExceptionResolver )放到dispatcherServlet中做准备:

0

0

0

...

但是这些Bean又是从哪来的呢?? 来来来, 回到我们的WebAppConfig

我们使用的一个@EnableWebMvc

  1. 导入了DelegatingWebMvcConfiguration@Import(DelegatingWebMvcConfiguration.class)
  2. DelegatingWebMvcConfiguration的父类就配置了这些Bean
  3. 而且我告诉你SpringBoot也是用的这种方式,

0

总结

  1. Tomcat在启动时会通过SPI注册 ContextLoaderListener和DispatcherServlet对象
    1. 同时创建父子容器
      1. 分别创建在ContextLoaderListener初始化时创建父容器设置配置类
      2. 在DispatcherServlet初始化时创建子容器 即2个ApplicationContext实例设置配置类
  2. Tomcat在启动时执行ContextLoaderListener和DispatcherServlet对象的初始化方法, 执行容器refresh进行加载
  3. 在子容器加载时 创建SpringMVC所需的Bean和预准备的数据:(通过配置类+@EnableWebMvc配置(DelegatingWebMvcConfiguration)——可实现WebMvcConfigurer进行定制扩展)
    1. RequestMappingHandlerMapping,它会处理@RequestMapping 注解
    2. RequestMappingHandlerAdapter,则是处理请求的适配器,确定调用哪个类的哪个方法,并且构造方法参数,返回值。
    3. HandlerExceptionResolver 错误视图解析器
    4. addDefaultHttpMessageConverters 添加默认的消息转换器(解析json、解析xml)
    5. 等....
  4. 子容器需要注入父容器的Bean时(比如Controller中需要@Autowired Service的Bean); 会先从子容器中找,没找到会去父容器中找: 详情见AbstractBeanFactory#doGetBean方法

 

/** 
 * 一般情况下,只有Spring 和SpringMvc整合的时才会有父子容器的概念, 
 * 作用:
 * 比如我们的Controller中注入Service的时候,发现我们依赖的是一个引用对象,那么他就会调用getBean去把service找出来
 * 但是当前所在的容器是web子容器,那么就会在这里的 先去父容器找
 */
BeanFactory parentBeanFactory = getParentBeanFactory();
//若存在父工厂,且当前的bean工厂不存在当前的bean定义,那么bean定义是存在于父beanFacotry中
if (parentBeanFactory != null && !containsBeanDefinition(beanName)) {
   //获取bean的原始名称
   String nameToLookup = originalBeanName(name);
   //若为 AbstractBeanFactory 类型,委托父类处理
   if (parentBeanFactory instanceof AbstractBeanFactory) {
      return ((AbstractBeanFactory) parentBeanFactory).doGetBean(
            nameToLookup, requiredType, args, typeCheckOnly);
   }
   else if (args != null) {
      //  委托给构造函数 getBean() 处理
      return (T) parentBeanFactory.getBean(nameToLookup, args);
   }
   else {
      // 没有 args,委托给标准的 getBean() 处理
      return parentBeanFactory.getBean(nameToLookup, requiredType);
   }

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

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

相关文章

面向长文本处理的键值缓存压缩技术:智能压缩,无损性能,免微调

随着输入长度的增加&#xff0c;大型语言模型&#xff08;LLMs&#xff09;中的键值&#xff08;KV&#xff09;缓存需要存储更多的上下文信息以维持性能&#xff0c;这导致内存消耗和计算时间急剧上升。KV缓存的增长对内存和时间效率的挑战主要表现在两个方面&#xff1a;一是…

【数据库初阶】SQL--DCL

文章目录 DCL1. 基本介绍2. 用户管理2.1 查询用户2.2 创建用户2.3 修改用户密码2.4 删除用户 3. 权限控制3.1 查询权限3.2 授予权限3.3 撤销权限 4. DCL总结 DCL 更多数据库MySQL系统内容就在以下专栏&#xff1a; 专栏链接&#xff1a;数据库MySQL 1. 基本介绍 DCL英文全称是…

推荐系统学习 二

双塔模型的结构 用户的特征&#xff0c;我们知道用户ID还能从用户填写的资料和用户行为中获取很多特征&#xff0c;包括离散特征和连续特征。所有这些特征不能直接输入神经网络&#xff0c;而是要先做一些处理&#xff0c;比如用embedding层把用户ID映射到一个向量 跟之前我们…

【每日刷题】Day56

【每日刷题】Day56 &#x1f955;个人主页&#xff1a;开敲&#x1f349; &#x1f525;所属专栏&#xff1a;每日刷题&#x1f34d; &#x1f33c;文章目录&#x1f33c; 1. 292. Nim 游戏 - 力扣&#xff08;LeetCode&#xff09; 2. 129. 求根节点到叶节点数字之和 - 力扣…

QT案例 记录解决在管理员权限下QFrame控件获取拖拽到控件上的文件路径

参考知乎问答 Qt管理员权限如何支持拖放操作&#xff1f; 的回答和代码示例。 解决在管理员权限运行下&#xff0c;通过窗体的QFrame子控件获取到拖拽的内容。 目录标题 导读解决方案详解示例详细 【管理员权限】在QFrame控件中获取拖拽内容 【管理员权限】继承 IDropTarget 类…

[SaaS] AI+数据,tiktok选品,找达人,看广告数据

TK观察专访丨前阿里“鲁班”创始人用AIGC赋能TikTok获千万融资用AI数据做TikTokhttps://mp.weixin.qq.com/s/xp5UM3ROo48DK4jS9UBMuQ主要还是爬虫做数据的。 商家做内容&#xff1a;1.找达人拍内容&#xff0c;2.商家自己做原生自制内容&#xff0c;3.广告内容。 短视频&…

俞敏洪一句“乱七八糟”,让东方甄选跌了40亿

“虽然直播的特点就是能说会道&#xff0c;但是网上那种买买买的嚎叫&#xff0c;我是完全看不起的”&#xff0c;俞敏洪在2023亚布力论坛上颇为自豪地说。 在他看来&#xff0c;“直播带货”本质上也是教育的一种&#xff0c;对产品进行知识性讲解才是最核心的一环。而知识传…

Centos 7部署NTP

介绍 NTP是Network Time Protocol&#xff08;网络时间协议&#xff09;的简称&#xff0c;它是用来通过互联网或局域网将计算机时钟同步到世界协调时间&#xff08;UTC&#xff09;的协议。 安装 # yum安装 yum install -y ntp# 离线安装 #下载地址&#xff1a;https://mir…

Meta Llama 3 残差结构

Meta Llama 3 残差结构 flyfish 在Transformer架构中&#xff0c;残差结构&#xff08;Residual Connections&#xff09;是一个关键组件&#xff0c;它在模型的性能和训练稳定性上起到了重要作用。残差结构最早由He et al.在ResNet中提出&#xff0c;并被广泛应用于各种深度…

【python】爬虫记录每小时金价

数据来源&#xff1a; https://www.cngold.org/img_date/ 因为这个网站是数据随时变动的&#xff0c;用requests、BeautifulSoup的方式解析html的话&#xff0c;数据的位置显示的是“--”&#xff0c;并不能取到数据。 所以采用webdriver访问网站&#xff0c;然后从界面上获取…

3389远程连接器,3389远程连接器如何进行远程连接

3389远程连接器是一款专业的远程桌面连接工具&#xff0c;它允许用户通过网络远程访问和控制另一台计算机&#xff0c;实现远程办公、技术支持、文件传输等多种功能。下面将详细介绍如何使用3389远程连接器进行远程连接。 首先&#xff0c;确保被连接的计算机已经开启了远程桌面…

TCP的核心属性

TCP的核心属性 一: TCP的核心属性1.1: 确认应答:1.2 : 超时重传1.3 : 连接管理1.3.1 三次握手1.3.2 四次挥手 1.4 滑动窗口1.5: 流量控制:1.6 拥塞控制1.7 延时应答1.8 :捎带应答1.9: 面向字节流1.10 : 异常情况 一: TCP的核心属性 1.1: 确认应答: 保证可靠性最核心的机制 1…

二刷算法训练营Day22 | 二叉树(8/9)

目录 详细布置&#xff1a; 1. 235. 二叉搜索树的最近公共祖先 2. 701. 二叉搜索树中的插入操作 3. 450. 删除二叉搜索树中的节点 详细布置&#xff1a; 1. 235. 二叉搜索树的最近公共祖先 给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。 百度百科中最近公共…

二叉树的算法题目

二叉树的遍历题目 二叉树遍历一般包含三种分别为&#xff1a;根左右、左根右、左右根&#xff08;又称为前序遍历、中序遍历、后序遍历&#xff09; 方法一&#xff1a;使用递归遍历 方法二&#xff1a;使用迭代使用栈 我们以左根右&#xff08;中序遍历&…

修复Windows上“发生意外错误”问题的5种方法,总有一种适合你

在尝试启动网络适配器的设置菜单时,是否收到“发生意外错误”消息?不用担心,因为在大多数情况下解决这个问题很容易。我们将向你展示在Windows 11或Windows 10计算机上解决此问题的多种方法。 为什么我收到“发生意外错误”的消息 当网络适配器出现问题时,Windows会显示一…

MariaDB数据导入与导出操作演示

文章目录 整个数据库导出导入先删除库然后再导入 参考这里&#xff1a; MariaDB数据库导出导入. 整个数据库 该部分演示&#xff1a;导出数据库&#xff0c;然后重建数据库&#xff0c;并导入数据的整个过程。 导出 Win R &#xff0c;打开运行输入cmd并回车&#xff0c;然…

【docker】docker的安装

如果之前安装了旧版本的docker我们需要进行卸载&#xff1a; 卸载之前的旧版本 卸载 # 卸载旧版本 sudo apt-get remove docker docker-engine docker.io containerd runc # 卸载历史版本 apt-get purge docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker…

如何在Weblogic环境中启动认证方式对接Zabbix监控

在WebLogic Server中&#xff0c;启动认证可用于确保只有经过授权的用户和系统能够访问WebLogic Server及其应用程序&#xff0c;通过合理配置认证提供者和安全领域&#xff0c;管理员可以有效管理和控制用户访问。 本文将详细介绍如何在Weblogic环境中配置启动认证并对接Zabb…

植物大战僵尸杂交版2.0.88最新版+防闪退工具V2+修改工具+高清工具

植物大战僵尸杂交版&#xff0c;不仅继承原作的经典玩法&#xff0c;而且引入了全新的植物融合玩法&#xff0c;将各式各样的植物进行巧妙的杂交&#xff0c;孕育出前所未有、功能各异的全新植物。 创新的杂交合成系统 游戏引入了创新的杂交合成系统&#xff0c;让玩家可以将不…

Swift 中的Getter 和 Setter

目录 前言 1. 什么是Getter和Setter 1.定义 2.作用 2.属性 1.存储属性 2.计算属性 3.属性观察者 3. 使用 Getter 和 Setter 的场景 1.数据转换 2.懒加载 3.数据验证和限制 4.触发相关操作 4.自定义Getter 和 Setter 5. 参考资料 前言 属性是 Swift 编程中的基本…