Spring源码篇(十一)注册bean的方式

news2024/7/6 20:10:50

文章目录

  • 前言
  • bean注册的方式
  • class扫描bean
    • @ComponentScan
    • @Import
      • DeferredImportSelector
      • ImportBeanDefinitionRegistrar
  • xml注册bean
  • spring扩展点
  • 总结

前言

本篇主要以注册bean的方式从源码角度展开分析总结。

bean注册的方式

首先,由spring管理的对象,称为bean,那么创建一个bean其实就是让spring创建一个bean,在创建bean时,先要注册bean,也就是进行class的解析,得到bean的定义beanDefinition,这个bean的定义保存了bean的名称,类全名,资源文件信息,注解信息等;

那么注册bean的方式有那些?

  1. class扫描,spring会自动扫描class路径,将含有注解如@PropertySource,@ComponentScan,@Import,@ImportResource,@Bean这些注解标注的class作为bean的class进行解析,得到beanDefinition
  2. xml方式,进行bean的配置,然后通过解析xml得到beanDefinition

class扫描bean

class扫描其实是找到class资源,然后解析class,得到beanDefinition,然后再解析beanDefinition,这个解析beanDefinition就是配置类解析;

位置:org.springframework.context.annotation.ConfigurationClassParser#doProcessConfigurationClass

在这里,他会解析注解@ComponentScan,@Import,@ImportResource,@PropertySource,@Bean,我们简单看下它的一个解析的大概流程;

假设现在是springBoot应用启动,那么它应该怎么走?

配置类上有@SpringBootApplication注解,它里面包含一个@Component注解,它一开始也就是应用启动时就会被注册为一个配置类,之后进行解析,也就是解析@ComponentScan,@Import,@ImportResource,@PropertySource,@Bean这几个注解,如下步骤:

  1. 解析嵌套的类
  2. 解析@PropertySource注解
  3. 解析@ComponentScan
  4. 解析@Import方法
  5. 解析@ImportResouce方法
  6. 解析@Bean方法
  7. 接口上的默认方法(default修饰)标注有@Bean也进行解析
  8. 获取父级类,然后返回,返回,就是一个递归调用
protected final SourceClass doProcessConfigurationClass(
			ConfigurationClass configClass, SourceClass sourceClass, Predicate<String> filter)
			throws IOException {

		if (configClass.getMetadata().isAnnotated(Component.class.getName())) {
			// 1. 解析嵌套的类,一个类里可能存在一个内部类,并且含有@Component
			processMemberClasses(configClass, sourceClass, filter);
		}

		// 2. 解析@PropertySource注解
    // 加载value值,将其读取并添加到environment中
		for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable(
				sourceClass.getMetadata(), PropertySources.class,
				org.springframework.context.annotation.PropertySource.class)) {
			if (this.environment instanceof ConfigurableEnvironment) {
				processPropertySource(propertySource);
			}
			else {
				logger.info("Ignoring @PropertySource annotation on [" + sourceClass.getMetadata().getClassName() +
						"]. Reason: Environment must implement ConfigurableEnvironment");
			}
		}

		// 3. 解析@ComponentScan
    // 这里解析时一个递归的过程,扫描指定路径下class,扫描到后,同样会走进这个方法
		Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(
				sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);
		if (!componentScans.isEmpty() &&
				!this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {
			for (AnnotationAttributes componentScan : componentScans) {
				// The config class is annotated with @ComponentScan -> perform the scan immediately
				Set<BeanDefinitionHolder> scannedBeanDefinitions =
						this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());
				// Check the set of scanned definitions for any further config classes and parse recursively if needed
				for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
					BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition();
					if (bdCand == null) {
						bdCand = holder.getBeanDefinition();
					}
					if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) {
						parse(bdCand.getBeanClassName(), holder.getBeanName());
					}
				}
			}
		}

		// 4. 解析@Import方法
   // 这里导入的类也会进行分类处理,有:ImportSelector, ImportBeanDefinitionRegistrar
    // 如果都不是,就是Component,同样的也是一个递归,走这个方法
		processImports(configClass, sourceClass, getImports(sourceClass), filter, true);

		// 5. 解析@ImportResouce方法
    // 这里通过environment解析后,添加到配置类中
		AnnotationAttributes importResource =
				AnnotationConfigUtils.attributesFor(sourceClass.getMetadata(), ImportResource.class);
		if (importResource != null) {
			String[] resources = importResource.getStringArray("locations");
			Class<? extends BeanDefinitionReader> readerClass = importResource.getClass("reader");
			for (String resource : resources) {
				String resolvedResource = this.environment.resolveRequiredPlaceholders(resource);
				configClass.addImportedResource(resolvedResource, readerClass);
			}
		}

		// 6. 解析@Bean方法
    // 解析后得到beanMethod,然后添加到配置类中
		Set<MethodMetadata> beanMethods = retrieveBeanMethodMetadata(sourceClass);
		for (MethodMetadata methodMetadata : beanMethods) {
			configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass));
		}

		// 7. 接口上的默认方法(default修饰)标注有@Bean也进行解析
		processInterfaces(configClass, sourceClass);

		// 8. 获取父级类,然后返回,返回,就是一个递归调用
		if (sourceClass.getMetadata().hasSuperClass()) {
			String superclass = sourceClass.getMetadata().getSuperClassName();
			if (superclass != null && !superclass.startsWith("java") &&
					!this.knownSuperclasses.containsKey(superclass)) {
				this.knownSuperclasses.put(superclass, configClass);
				// Superclass found, return its annotation metadata and recurse
				return sourceClass.getSuperClass();
			}
		}

		// No superclass -> processing is complete
		return null;
	}

@ComponentScan

@ComponentScan就是扫描class,默认是取当前配置类的类路径进行扫描,底层位置:

org.springframework.context.annotation.ClassPathBeanDefinitionScanner#doScan

底层之前的文章有解析过spring源码篇(六)配置类解析过程,这里不做分析,我们看图

image-20230927163529139

这里它通过递归扫描,得到对应路径下的所有beanDefinition,然后调用parse递归解析配置类,

那么要被spring扫描器识别为一个需要注入的bean对象,这个类,就必须有@Component,@ManagedBean两个注解,我们常用的比如@Controller,@Service,@Configuration这些注解,里面都包含了一个@Component注解,所有都能被扫描到,那么另一个@ManagedBean是JSF定义bean的注解,不是很了解,知道就行;

那么这里得出一个结论:类上含有@Component注解,就可以被注册

@Import

image-20230927171330009

内部对import注解能处理的类分了3个类别:

  1. ImportSelector实现类;

    会区分是不是DeferredImportSelector实现类,这个也是自动配置导入类,不是这个类则执行selectImports方法,然后递归

  2. ImportBeanDefinitionRegistrar实现类

    这个是beanDefinition注册类,在配置类解析完会执行registerl类的registerBeanDefinition方法

  3. Component标注的类

image-20230928084729191

DeferredImportSelector

SpringBoot源码篇(一)自动配置

ImportBeanDefinitionRegistrar

ImportBeanDefinitionRegistrar调用的位置:org.springframework.context.annotation.ConfigurationClassPostProcessor#processConfigBeanDefinitions

image-20230926171415370

image-20230928085140814

	private void loadBeanDefinitionsFromRegistrars(Map<ImportBeanDefinitionRegistrar, AnnotationMetadata> registrars) {
        // 遍历ImportBeanDefinitionRegistrar集合
		registrars.forEach((registrar, metadata) ->
                           // 执行registerBeanDefinitions,也就是我们重写的方法
				registrar.registerBeanDefinitions(metadata, this.registry, this.importBeanNameGenerator));
	}

那么这里就是spring提供的扩展点调用的地方,我们看一下它提供了哪些方法:

image-20230928090311555

包含,获取bean定义,注册和移除bean定义,很好,到这里,我们已经可以实现bean的修改,删除,移除这些操作了,因为bean都是根据bean定义来创建的;

我们今天的主题是注册bean的问题,那么我这里也简单注册一个bean:

public class TestIm implements ImportBeanDefinitionRegistrar {
    /**
     * 
     * @param importingClassMetadata 配置类的注解元数据
     * @param registry bean定义注册器
     * @param importBeanNameGenerator beanName生成器
     */
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry,
                                        BeanNameGenerator importBeanNameGenerator) {
        // 定义一个新的bean定义
        RootBeanDefinition rootBeanDefinition = new RootBeanDefinition();
        rootBeanDefinition.setBeanClass(BeanUtil.class);
        // 注册我们自己的bean定义
        String beanName = importBeanNameGenerator.generateBeanName(rootBeanDefinition, registry);
        registry.registerBeanDefinition(beanName, rootBeanDefinition);
    }
}

这里注册的是hutool的工具beanUtil,可以从图中看到,已经注册上去了

image-20230928193906078

到这里,我们对@Import总结下:

  1. 可以直接导入一个类作为bean
  2. 导入的类实现了ImportSelector,可通过selectImports方法获取要导入的类,那么这里就分两种情况:
    1. 实现普通的ImportSelector类,通过selectImports方法获取需要注册bean
    2. 实现DeferredImportSelector类,这个是自动配置所实现的类,当然我们也可以实现
  3. 导入的类实现了ImportBeanDefinitionRegistrar,在registerBeanDefinitions方法中通过BeanDefinitionRegistry操作bean定义

xml注册bean

我们先创建一个xml,xml中定义了一个beanName为test7的bean,

<?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.xsd">

    <bean id="test7" class="com.liry.te.Test7" scope="singleton"/>
</beans>

然后配置类上通过@ImportResource(value = "classpath:spring-beans.xml")引入(我这里用的Spring Boot测试)

位置:org.springframework.context.annotation.ConfigurationClassParser#doProcessConfigurationClass

image-20230928203914865

这里它是获取的是ImportResourcelocations属性值作为资源文件的地址,这个属性是被链接到value上的,所以我们的配置是没问题的,然后它还获取了一个BeanDefinitionReader的读取器,这个它是默认的,看下图注解@ImportResource的定义,然后在获取到地址后,都添加到配置类中,这个和@Bean的处理方式一样;

image-20230928204128387

之后就是加载beanDefinition

image-20230926171415370

image-20230928204456647

这里就是真正进行资源文件解析和注册的地方

	private void loadBeanDefinitionsFromImportedResources(
			Map<String, Class<? extends BeanDefinitionReader>> importedResources) {

		Map<Class<?>, BeanDefinitionReader> readerInstanceCache = new HashMap<>();

		importedResources.forEach((resource, readerClass) -> {
			// 1. 选择默认的bean定义读取器,这里只是指定了类型
            // 它提供了两个:一个是groovy文件的,另一个是xml文件的
			if (BeanDefinitionReader.class == readerClass) {
				if (StringUtils.endsWithIgnoreCase(resource, ".groovy")) {
					// groovy文件的bean定义读取器
					readerClass = GroovyBeanDefinitionReader.class;
				}
				else {
					// xml文件的bean定义读取器
					readerClass = XmlBeanDefinitionReader.class;
				}
			}
			// 2. 拿实例化好的读取器
			BeanDefinitionReader reader = readerInstanceCache.get(readerClass);
            // 拿不到,因为是空的
			if (reader == null) {
				try {
					// 通过反射实例化读取器
					reader = readerClass.getConstructor(BeanDefinitionRegistry.class).newInstance(this.registry);
					// 赋予额外功能
					if (reader instanceof AbstractBeanDefinitionReader) {
						AbstractBeanDefinitionReader abdr = ((AbstractBeanDefinitionReader) reader);
						abdr.setResourceLoader(this.resourceLoader);
						abdr.setEnvironment(this.environment);
					}
                    // 添加缓存,避免每次都实例化
					readerInstanceCache.put(readerClass, reader);
				}
				catch (Throwable ex) {
					throw new IllegalStateException(
							"Could not instantiate BeanDefinitionReader class [" + readerClass.getName() + "]");
				}
			}

			// 3. 使用选择的读取器实例进行加载bean定义信息
			reader.loadBeanDefinitions(resource);
		});
	}

到这里我就不再进行深入了,下面的步骤大概的是,读取资源文件也就是io操作,然后解析xml,设置属性,构建beanDefinition对象。

对Xml方式的总结:

通过xml方式配置bean定义的属性信息,通过XmlBeanDefinitionReader注册到spring容器

spring扩展点

class扫描中一个特殊的方式,就是spring扩展点的方式,上面说的那些东西,其实也是通过spring扩展点实现的,它是由ConfigurationClassPostProcessor实现BeanDefinitionRegistryPostProcessor接口而达到的功能,同样我们也可以利用这些扩展点进行我们bean的注册;

位置:org.springframework.context.support.PostProcessorRegistrationDelegate#invokeBeanFactoryPostProcessors(org.springframework.beans.factory.config.ConfigurableListableBeanFactory, java.util.List<org.springframework.beans.factory.config.BeanFactoryPostProcessor>)

代码有点长就不贴了,其逻辑基本上就是获取2个后置处理器进行执行:

  1. BeanFactoryPostProcessor

  2. BeanDefinitionRegistryPostProcessor:bean的扫描和解析就是实现这个接口的

BeanFactoryPostProcessor方式注册一个bean:

@Component
public class TestIm2 implements BeanFactoryPostProcessor {
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        String beanName = "test8";
        Test8 test8 = new Test8();
        beanFactory.registerSingleton(beanName, test8);
    }
}

可以看到,这种方式和之前的都不一样,这里不是注册一个beanDefinition,而是一个实例,这里我们拿到的参数是ConfigurableListableBeanFactory类型的,提供了操作bean的功能;

另一种则是实现BeanDefinitionRegistryPostProcessor接口

@Component
public class TestIm3 implements BeanDefinitionRegistryPostProcessor {

    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
        GenericBeanDefinition genericBeanDefinition = new GenericBeanDefinition();
        genericBeanDefinition.setBeanClassName("com.liry.te.Test9");
        registry.registerBeanDefinition("com.liry.te.Test9", genericBeanDefinition);
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        // 这里就和BeanFactoryPostProcessor一样
    }
}

总结

注册bean分有:class扫描,xml扫描;

而注册的方法也都是围绕这两种方式来的,那我们对上面知识点进行总结:

  1. spring扫描bean,当类上有@Component,@ManagedBean标注时,能被扫描到并解析,然后注册
  2. spring扫描bean,通过注解@ComponentScan扫描指定路径下的bean,然后注册
  3. 扫描的bean上有注解@Import注解,如果是ImportSelector实现类,会执行selector导入方法,进而注册我们指定的bean,那这里还有一个隐藏点,就是以自动配置的方式进行注册,也就是spring的SPI机制;如果是ImportBeanDefinitionRegistrar实现类,可以通过注册器注册我们指定的bean定义
  4. xml文件方式配置bean定义信息,通过导入到spring容器(@ImportResource)实现注册
  5. 通过实现这两个接口BeanFactoryPostProcessorBeanDefinitionRegistryPostProcessor完成spring扩展点的注入,也可以实现bean的注册;有一个不一样的点就是BeanFactoryPostProcessor接口它提供的并不是bean定义的注册,而是直接注册一个实例对象

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

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

相关文章

数学建模三大类模型适用场景及建模方法(纯干货)(3)

目录 一&#xff0c;评价类算法 1&#xff0c;层次分析法 ●基本思想: ●基本步骤: ●优点: ●缺点 ●适用范围: ●改进方法: 2&#xff0c;灰色综合评价法&#xff08;灰色关联度分析&#xff09; ●基本思想: ●基本步骤: ●优点: ●缺点: ●适用范围: ●改进方…

海外媒体发稿:商务视频推广销售利器之完全指南

在当今数字化时代&#xff0c;商务视频推广已经成为了企业获取市场份额和提升销售业绩的重要手段。视频作为一种视听媒体&#xff0c;拥有更强大的感染力和传达信息的能力&#xff0c;因此在各种销售场景中得到了广泛应用。本文为大家提供了一份完全指南&#xff0c;帮助你了解…

为什么炒股人更爱融资?融券交易背后的风险与获利机会

炒股过程中&#xff0c;融资和融券交易是常见的操作方式。然而&#xff0c;据观察&#xff0c;炒股的人更倾向于选择融资交易&#xff0c;而融券交易相对较少。那么&#xff0c;是什么导致了这种偏好呢&#xff1f;本文将解析融资和融券交易的运作机制&#xff0c;以及投资者为…

【Linux】TCP的服务端 + 客户端

文章目录 &#x1f4d6; 前言1. 服务端基本结构1.1 类成员变量&#xff1a;1.2 头文件1.3 初始化&#xff1a;1.3 - 1 全双工与半双工1.3 - 2 inet_aton1.3 - 3 listen 2. 服务端运行接口2.1 accept&#xff1a;2.2 服务接口&#xff1a; 3. 客户端3.1 connect&#xff1a;3.2 …

RSIC-V工具链介绍及其安装教程

前言 &#xff08;1&#xff09;此系列文章是跟着汪辰老师的RISC-V课程所记录的学习笔记。 &#xff08;2&#xff09;该课程相关代码gitee链接&#xff1b; &#xff08;3&#xff09;PLCT实验室实习生长期招聘&#xff1a;招聘信息链接 &#xff08;4&#xff09;在配置RSIC-…

私有云OpenStack保姆级教学

一、Openstack介绍 OpenStack是由美国国家航空航天局(NASA)与Rackspace公司合作研发并发起的&#xff0c;以Apache许可证授权的自由软件和开放源代码的云计算技术解决方案&#xff0c;其是一个项目也是一个软件&#xff0c;主要用于实现云项目&#xff0c;因云项目操作系统而存…

“把握拐点,洞悉投资者情绪与比特币价格的未来之路!“

“本来这篇文章是昨天晚上发的&#xff0c;国庆节庆祝喝多了&#xff0c;心有余而力不足&#xff01;直接头躺马桶GG了” 标准普尔 500 指数 200 天移动平均线云是我几个月来一直分享的下行目标&#xff0c;上周正式重新测试了该目标。200 日移动平均线云表示为: 200 天指数移…

iMazing 2.17.10官方中文版含2023最新激活许可证码

iMazing 2.17.10官方中文版是一款iOS设备管理软件&#xff0c;该软件支持对基于iOS系统的设备进行数据传输与备份&#xff0c;用户可以将包括&#xff1a;照片、音乐、铃声、视频、电子书及通讯录等在内的众多信息在Windows/Mac电脑中传输/备份/管理。 iMazing 2.17.10官方中文…

JavaSE | 初识Java(八) | 类和对象

在 java 中定义类时需要用到 class 关键字 &#xff0c;具体语法如下 // 创建类 class ClassName{field; // 字段(属性) 或者 成员变量method; // 行为 或者 成员方法 } class 为 定义类的关键字&#xff0c; ClassName 为类的名字&#xff0c; {} 中为类的主体。 类中包含的内…

笔试强训Day11

T1&#xff1a;二叉树 链接&#xff1a;二叉树_牛客题霸_牛客网 (nowcoder.com)​​​​​​​s 题意&#xff1a;给你一颗二叉树&#xff0c;求俩个点的最近公共祖先&#xff08;LCA&#xff09; 因为比较特殊&#xff0c;树是一颗二叉树&#xff0c;二叉树的编号很特殊&…

Multisim 14.3如何修改默认安装路径及下载

Multisim 14.3默认安装到C盘&#xff0c;而且没有修改安装路径选项&#xff0c;给安装带来了很多不便&#xff0c;经过网络查询、实际操作&#xff0c;成功安装到了D盘&#xff0c;希望对想修改默认安装路径的朋友有所帮助。 一、安装前准备工作&#xff0c;以下实操真对初学者…

OpenNebula的配置与应用

学习了OpenNebula的安装之后&#xff0c;接下来就是配置OpenNebula&#xff0c;内容包括配置Sunstone&#xff0c;VDC和集群&#xff0c;设置影像&#xff0c;模板管理&#xff0c;虚拟机管理等。OpenNebula还有大量的工作要做&#xff0c;这些工作主要来自映像、模板和虚拟机管…

Spring Boot的创建和使用(JavaEE进阶系列2)

目录 前言&#xff1a; 1.什么是Spring Boot&#xff1f;为什么要学习Spring Boot&#xff1f; 2.Spring Boot优点 3.创建Spring Boot项目 3.1准备工作 3.2Spring Boot创建 3.2.1通过idea的方式创建 3.2.2通过网页创建 4.Spring Boot中的配置文件 4.1Spring Boot配置…

树莓集团涉足直播产业园区运营,成都直播产业园区再添黑马

树莓集团涉足成都直播产业园运营领域&#xff0c;这一消息引起了业界的广泛关注。在这个无限可能的直播领域中&#xff0c;树莓集团将与上市公司德商产投紧密合作&#xff0c;立志为成都直播行业的发展注入新的活力。成都天府蜂巢直播产业园推行着一系列创新的政策措施&#xf…

算法通过村第十一关-位运算|黄金笔记|位运算压缩

文章目录 前言用4kb内存寻找重复元素总结 前言 提示&#xff1a;如果谁对你说了地狱般的话&#xff0c;就代表了他的心在地狱。你不需要相信那样的话&#xff0c;就算对方是你的父母也一样。 --高延秀《远看是蔚蓝的春天》 位运算有个很重要的作用就是能用比较小的空间存储比较…

Tensorflow、Pytorch和Ray(张量,计算图)

1.深度学习框架&#xff08;Tensorflow、Pytorch&#xff09; 1.1由来 可以追溯到2016年&#xff0c;当年最著名的事件是alphago战胜人类围棋巅峰柯洁&#xff0c;在那之后&#xff0c;学界普遍认为人工智能已经可以在一些领域超过人类&#xff0c;未来也必将可以在更多领域超过…

网盘搜索引擎:点亮知识星空,畅享数字宝藏!

大家好&#xff01;作为一名资深的网络产品运营人员&#xff0c;我今天要向大家介绍一款让你受益匪浅的神奇工具——网盘搜索引擎&#xff01;它可以帮助你免费搜索查询各种云盘共享资源&#xff0c;包括影视作品、纪录片、小说、动漫等等。现在&#xff0c;我们急需网络流量&a…

手边酒店V2独立版小程序 1.0.21 免授权+小程序前端安装教程

手边酒店小程序独立版酒店宾馆订房系统支持创建多个小程序&#xff0c;让每一个客户单独管理属于自己的小程序。系统无需授权&#xff0c;小程序端用户授权也是采用最新接口。 缺点不开源不影响使用&#xff0c;播播资源安装测试下来未发现或出现BUG情况&#xff0c;用户授权接…

深度学习 图像分割 PSPNet 论文复现(训练 测试 可视化)

Table of Contents 一、PSPNet 介绍1、原理阐述2、论文解释3、网络模型 二、部署实现1、PASCAL VOC 20122、模型训练3、度量指标4、结果分析5、图像测试 一、PSPNet 介绍 PSPNet(Pyramid Scene Parsing Network)来自于CVPR2017的一篇文章&#xff0c;中文翻译为金字塔场景解析…

Redis主从复制、哨兵、cluster集群

目录 Redis 主从复制 主从复制的作用 主从复制流程 搭建Redis 主从复制 实验环境 所有主机安装redis 修改 Redis 配置文件&#xff08;Master节点操作&#xff09; 修改 Redis 配置文件&#xff08;Slave节点操作&#xff09; 验证主从效果 Redis 哨兵模式 哨兵模式的…