SpringBoot的自动装配原理、自定义Starter与SPI机制

news2024/11/25 20:20:55

一、前言

Spring简直是java企业级应用开发人员的春天,我们可以通过Spring提供的ioc容器,避免硬编码带来的程序过度耦合。

但是,启动一个Spring应用程序也绝非易事,他需要大量且繁琐的xml配置,开发人员压根不能全身心的投入到业务中去。

因此,SpringBoot诞生了,虽然本质上还是属于Spring,但是SpringBoot的优势在于以下两个特点:

(1)约定大于配置

SpringBoot定义了项目的基本骨架,例如各个环境的配置文件统一放到resource中,使用active来启用其中一个。配置文件默认为application.properties,或者yaml、yml都可以。

(2)自动装配

以前在Spring使用到某个组件的时候,需要在xml中对配置好各个属性,之后被Spring扫描后注入进容器。

而有了SpringBoot后,我们仅仅需要引入一个starter,就可以直接使用该组件,如此方便、快捷,得益于自动装配机制。


二、自动装配原理

我们从SpringBoot的主入口开始

@SpringBootApplicationpublicclassYmApplication {publicstaticvoidmain(String[] args) {        SpringApplication.run(YmApplication.class, args);    }}

这个类最大的特点就是使用了@SpringBootApplication注解,该注解用于标注主配置类。

这样SpringBoot在启动的时候,就会运行这个类的run方法。

@SpringBootApplication

@SpringBootApplication注解又是一个组合注解

@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documented@Inherited@SpringBootConfiguration@EnableAutoConfiguration@ComponentScan(excludeFilters = {		@Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),		@Filter(type = FilterType.CUSTOM,				classes = AutoConfigurationExcludeFilter.class) })public@interface SpringBootApplication {...}

其中@Target @Retention@Documented@Inherited是元注解,即对注解的注解,可以移步我的另外一篇文章来了解它们使用自定义注解简易模拟Spring中的自动装配@Autowired

还包含了@SpringBootConfiguration@EnableAutoConfiguration

以下注解都将省略这些元注解

@SpringBootConfiguration

@Configurationpublic@interface SpringBootConfiguration {}@Componentpublic@interface Configuration {@AliasFor(        annotation = Component.class    )    String value()default"";}

可以看到,@SpringBootConfiguration注解本质上是一个@Configuration注解,表明该类是一个配置类

@Configuration又被@Component注解修饰,代表任何加了@Configuration注解的配置类,都会被注入进Spring容器中

@EnableAutoConfiguration

该注解开启了自动配置的功能

@AutoConfigurationPackage@Import(AutoConfigurationImportSelector.class)public@interface EnableAutoConfiguration {...}

本身又包含了以下两个注解

@AutoConfigurationPackage@Import(AutoConfigurationImportSelector.class)

@AutoConfigurationPackage

以前我们直接使用Spring的时候,需要在xml中的context:component-scan中定义好base-package那么Spring在启动的时候,就会扫描该包下及其子包下被@Controller、@Service与@Component标注的类,并将这些类注入到容器中

@AutoConfigurationPackage则会将被注解标注的类,即主配置类,将主配置类所在的包当作base-package,而不用我们自己去手动配置了。

这也就是为什么我们需要将主配置类放在项目的最外层目录中的原因。

那么容器是怎么知道主配置当前所在的包呢?

我们注意到,@AutoConfigurationPackage中使用到了@Import注解

@Import注解会直接向容器中注入指定的组件

引入了AutoConfigurationPackages类中内部类Registrar

	staticclassRegistrarimplementsImportBeanDefinitionRegistrar, DeterminableImports {		@Override		publicvoidregisterBeanDefinitions(AnnotationMetadata metadata,				BeanDefinitionRegistry registry) {			register(registry, newPackageImport(metadata).getPackageName());		}		@Override		public Set<Object> determineImports(AnnotationMetadata metadata) {			return Collections.singleton(newPackageImport(metadata));		}	}

debug后可以看到,metadata是主配置类

而getName将会返回主配置类所在的包路径

这样,容器就知道了主配置类所在的包,之后就会扫描该包及其子包。

@Import(AutoConfigurationImportSelector.class)

该注解又引入了AutoConfigurationImportSelector

AutoConfigurationImportSelector中有一个可以获取候选配置的方法,即getCandidateConfigurations

	protected List<String> getCandidateConfigurations(AnnotationMetadata metadata,			AnnotationAttributes attributes) {		List<String> configurations = SpringFactoriesLoader.loadFactoryNames(				getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader());		Assert.notEmpty(configurations,				"No auto configuration classes found in META-INF/spring.factories. If you "						+ "are using a custom packaging, make sure that file is correct.");		return configurations;	}	protected Class<?> getSpringFactoriesLoaderFactoryClass() {		return EnableAutoConfiguration.class;	}

其中核心方法SpringFactoriesLoader.loadFactoryNames,第一个参数是EnableAutoConfiguration.class

loadFactoryNames方法

	publicstaticfinalStringFACTORIES_RESOURCE_LOCATION="META-INF/spring.factories";	publicstatic List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) {		StringfactoryClassName= factoryClass.getName();		return loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());	}	privatestatic Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {		MultiValueMap<String, String> result = cache.get(classLoader);		if (result != null) {			return result;		}		try {			Enumeration<URL> urls = (classLoader != null ?					classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :					ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));			result = newLinkedMultiValueMap<>();			while (urls.hasMoreElements()) {				URLurl= urls.nextElement();				UrlResourceresource=newUrlResource(url);				Propertiesproperties= PropertiesLoaderUtils.loadProperties(resource);				for (Map.Entry<?, ?> entry : properties.entrySet()) {					StringfactoryClassName= ((String) entry.getKey()).trim();					for (String factoryName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {						result.add(factoryClassName, factoryName.trim());					}				}			}			cache.put(classLoader, result);			return result;		}		catch (IOException ex) {			thrownewIllegalArgumentException("Unable to load factories from location [" +					FACTORIES_RESOURCE_LOCATION + "]", ex);		}	}

可以看得出,loadSpringFactorie方法,会从META-INF/spring.factories文件中读取配置,将其封装为Properties对象,将每个key作为返回的map的key,将key对应的配置集合作为该map的value。

loadFactoryNames则是取出key为EnableAutoConfiguration.class的配置集合

我们查看META-INF/spring.factories的内容(完整路径:org\springframework\boot\spring-boot-autoconfigure\2.1.4.RELEASE\spring-boot-autoconfigure-2.1.4.RELEASE.jar!\META-INF\spring.factories)

可以看到,EnableAutoConfiguration对应的value,则是我们在开发中经常用到的组件,比如Rabbit、Elasticsearch与Redis等中间件。

到这里,我们可以知道getCandidateConfigurations方法会从META-INF/spring.factories中获取各个组件的自动配置类的全限定名。这么多自动配置类,难道是一启动SpringgBoot项目,就会全部加载吗?

那显然不是的,我们点进其中的一个自动配置类中,例如是org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration

@Configuration@ConditionalOnClass(RedisOperations.class)@EnableConfigurationProperties(RedisProperties.class)@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })publicclassRedisAutoConfiguration {	@Bean	@ConditionalOnMissingBean(name = "redisTemplate")	public RedisTemplate<Object, Object> redisTemplate(			RedisConnectionFactory redisConnectionFactory)throws UnknownHostException {		RedisTemplate<Object, Object> template = newRedisTemplate<>();		template.setConnectionFactory(redisConnectionFactory);		return template;	}	@Bean	@ConditionalOnMissingBean	public StringRedisTemplate stringRedisTemplate(			RedisConnectionFactory redisConnectionFactory)throws UnknownHostException {		StringRedisTemplatetemplate=newStringRedisTemplate();		template.setConnectionFactory(redisConnectionFactory);		return template;	}}

可以看到,该自动配置类中,确实提供了RedisTemplate与StringRedisTemplate的Bean。

但是我们注意到上面的注解,

@EnableConfigurationProperties(RedisProperties.class)

使得被@ConfigurationProperties修饰的类生效,RedisProperties就是被@ConfigurationProperties修饰,即会将RedisProperties类注入到容器中。

@ConfigurationProperties(prefix = "spring.redis")publicclassRedisProperties {	privateintdatabase=0;	private String url;	privateStringhost="localhost";	private String password;    ...}

@ConfigurationProperties(prefix = "spring.redis")则会将application.yml中以spring.redis开头的配置映射到该类中。

@ConditionalOnClass(RedisOperations.class)

当前的类路径下存在RedisOperations.class时,才会加载RedisAutoConfiguration配置类。

同样的注解还有

@ConditionalOnBean:当容器里有指定Bean的条件下

@ConditionalOnMissingBean:当容器里没有指定Bean的情况下

@ConditionalOnMissingClass:当容器里没有指定类的情况下

那怎么样才能加载RedisAutoConfiguration类呢?

这就需要我们在pom中引入redis的starter,即spring-boot-starter-data-redis。我们以2.1.4.RELEASE版本为例。该版本的starter又会引入2.1.6.RELEASE版本的spring-data-redis的依赖,spring-data-redis中会有RedisOperations

全路径为spring-data-redis\2.1.6.RELEASE\spring-data-redis-2.1.6.RELEASE.jar!\

org\springframework\data\redis\core\RedisOperations.class

我们结合redis总结下SpringBoot的自动装配流程


三、如何自定义一个starter

我们实现一个简单的功能吧,实现一个LRU缓存(对LRU不熟悉的同学,可以先移步我的这篇文章Redis的键过期策略、内存淘汰机制与LRU实现,这一篇给你安排了!

从第二节的末尾来看,redis的starter中,包含以下几个核心构件:

(1)自动配置类RedisAutoConfiguration ,并且向容器中注入RedisTemplate

(2)用于映射以spring.redis为前缀的配置的类RedisProperties

(3)用于操作redis的RedisOperation接口,RedisTemplate是对其的实现

(4)在spring.factories中将RedisAutoConfiguration添加进EnableAutoConfiguration的vaule集合中

那我们新建一个maven项目,这是我的项目结构:

pom文件内容为:

<?xml version="1.0" encoding="UTF-8"?><projectxmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.1.4.RELEASE</version><relativePath/><!-- lookup parent from repository --></parent><groupId>com.y</groupId><artifactId>lru-spring-boot-starter</artifactId><version>0.0.1-SNAPSHOT</version><name>lru-spring-boot-starter</name><properties><java.version>1.8</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency></dependencies></project>

操作lru的类:

/** * @author qcy * @create 2021/08/23 22:19:38 */publicclassLRUService {private LRUCache lruCache;    LRUService(LRUProperties properties) {        lruCache = newLRUCache(properties.getCapacity());    }publicvoidput(Integer key, Integer value) {        lruCache.put(key, value);    }public Integer get(Integer key) {return lruCache.get(key);    }public String print() {return lruCache.print();    }staticclassLRUCache {//维护位置的LinkedList,set()的时间复杂度O(n),但如果只操作头尾元素,则时间复杂度为O(1)private LinkedList<Integer> list;//维护键值的HashMap,get()的时间复杂度O(1)private HashMap<Integer, Integer> map;//缓存的容量privateint capacity;        LRUCache(int capacity) {this.list = newLinkedList<>();this.map = newHashMap<>();this.capacity = capacity;        }private Integer get(Integer key) {if (map.get(key) == null) {//说明缓存中没有该keyreturnnull;            }//缓存中有该key,则先将该key在链表中删除,再移动到链表的尾部,从而保证头部是最近最久未使用的元素            list.remove(key);            list.offer(key);return map.get(key);        }privatevoidput(Integer key, Integer value) {if (map.get(key) != null) {//说明缓存中有该key,先在链表中删除,再移动到尾部                list.remove(key);                list.offer(key);            } else {//说明缓存中没有该key,需要往缓存中插入if (list.size() == capacity) {//说明缓存已经满了//删除链表头部元素Integerhead= list.poll();//删除键值対                    map.remove(head);                }//此时缓存没满,或刚删除了头部元素                list.offer(key);            }//插入map或刷新vaule            map.put(key, value);        }//输出缓存内元素private String print() {StringBuildersb=newStringBuilder();for (inti= list.size() - 1; i >= 0; i--) {Integerkey= list.get(i);Integervalue= map.get(key);                sb.append("(").append(key).append(",").append(value).append(")").append("\n");            }return sb.toString();        }    }}

配置类:

@ConfigurationProperties(prefix = "lru")publicclassLRUProperties {private Integer capacity;public Integer getCapacity() {return capacity;    }publicvoidsetCapacity(Integer capacity) {this.capacity = capacity;    }}

LRU的自动配置类:

/** * @author qcy * @create 2021/08/23 22:25:31 */@Configuration@EnableConfigurationProperties(LRUProperties.class)publicclassLRUAutoConfiguration {@Autowired    LRUProperties properties;@Bean@ConditionalOnMissingBeanpublic LRUService lruService() {returnnewLRUService(properties);    }}

在resource目录下新建META-INF文件夹,新建spring.factories文件

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\com.y.lru.LRUAutoConfiguration

最后使用mvn clean install打成本地jar

然后在其他项目中,引用该jar

<dependency><groupId>com.y</groupId><artifactId>lru-spring-boot-starter</artifactId><version>0.0.1-SNAPSHOT</version></dependency>

并且设置一下lru缓存的大小

lru.capacity=2

现在测试一下:

@Autowired    LRUService lruService;@RequestMapping("put")private String put(@RequestParam Integer key, @RequestParam Integer value) {        lruService.put(key, value);return"ok";    }@RequestMapping("get")private Integer get(@RequestParam Integer key) {return lruService.get(key);    }@RequestMapping("print")private String print() {return lruService.print();    }

先后put(1,1)、(2,2)与(3,3),因为缓存大小是2,所以直接打印后可以得到以下结果,越先输出,代表越是最近使用的。


四、SpringBoot与JDK中的SPI机制

这里我们先谈谈SpringBoot中的spi机制

什么是spi呢,全称是Service Provider Interface。简单翻译的话,就是服务提供者接口,是一种寻找服务实现的机制。

我举一个生活中的例子吧,汽车的轮胎是可以更换的吧,不可能厂家直接将轮胎焊死在汽车上的,你大可以换成其他制造商的轮胎,但总不可能换上自行车的轮胎。

那么这里的轮胎就是可插拔的,只要满足厂家制定的规范,汽车就可以正常上路行驶。

写代码也是一样的,有时候我不想直接写死具体的实现类,否则,如果要更换实现类的话,就需要修改代码。为了让能实现类具有可插拔的特性,我可以定义一个规范,只要外部厂家按照规范去做,我就可以去动态地去发现这些实现类。

SpringBoot为了实现组件的动态插拔,定义了这样一套规范:SpringBoot在启动的时候,会扫描所有jar包resource/META-INF/spring.factories文件,依据类的全限定名,利用反射机制将Bean装载进容器中。

所以呢,只要外部的jar按照这套规范做事,就可能将自己的功能为SpringBoot所用,上一节的自定义starter其实就是在实现这一套规范。

spi机制呢,就是会利用额外的一个配置文件,来完成对组件的动态装载,从而实现解耦。

所以,对于SpringBoot的spi机制,用一句话概括:

SpringBoot利用SpringFactoriesLoader将spring.factories内容映射为Properties,利用反射实例化Bean并注入进容器,来实现组件的动态插拔,实现解耦。

那么jdk中的spi机制呢?

先从一个简单的例子开始:

定义一个日志接口,内部有一个打印方法

package com.yang.ym.spi;/** * @author qcy * @create 2021/08/24 23:15:27 */publicinterfaceLog {publicvoidprint();}

有两个实现类,一个是控制台日志

package com.yang.ym.spi;/** * @author qcy * @create 2021/08/24 23:15:39 */publicclassConsoleLogimplementsLog {@Overridepublicvoidprint() {        System.out.println("在控制台里打印日志");    }}

还有一个实现类是文件日志:

package com.yang.ym.spi;/** * @author qcy * @create 2021/08/24 23:16:02 */publicclassFileLogimplementsLog {@Overridepublicvoidprint() {        System.out.println("在文件里打印日志");    }}

接着我们需要按照jdk中spi的规范

在resources目录下,新建META-INF\services目录,在services目录底下新建com.yang.ym.spi.Log目录,即接口的全限定名,在该全限定名目录底下,以实现类的全限定名新建两个文件,一个是com.yang.ym.spi.ConsoleLog,另外一个是com.yang.ym.spi.FileLog,如下图所示:

最后利用ServiceLoader去发现这些实体类

publicstaticvoidmain(String[] args) {        ServiceLoader<Log> logServiceLoader = ServiceLoader.load(Log.class);for (Log log : logServiceLoader) {            log.print();        }    }

logServiceLoader就是实现类的集合,最后的效果:

ServiceLoader内部会借助一个LazyIterator,因为增强型for循环会被编译为Iterator,而LazyIterator实现了Iterator,其hasNext()方法会去寻找下一个服务实现类,next()方法才会利用反射实例化该实现类,起到一种懒加载的作用,故命名为LazyIterator。

可以看得出,SpringBoot与jdk在spi机制上,存在些许的差别,但本质上还是事先定义一套规范,来完成对实现类或者组件的动态发现。

在获取实现类名称集合的层面上,SpringBoot借助于SpringFactoriesLoader加载spring.factories配置文件,而jdk借助于ServiceLoader读取指定路径。

在是否实例化实现类的层面上,SpringBoot会依据Conditional注解来判断是否进行实例化并注入进容器中,而jdk会在next方法内部懒加载实现类。

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

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

相关文章

学习国家颁布的三部信息安全领域法律,理解当前工作中的信息安全合规要求

目录三部信息安全领域的法律文件三部法律的角色定位与联系三部法律的适用范围三部法律的主要履职部门三部法律条文章节结构中的共性三部法律中的一些次重点章节网络安全法的重点章节数据安全法的重点章节个人信息保护法的重点章节关于工业和信息化部行政执法项目清单三部信息安…

ubuntu本地访问nas

需求 本地磁盘空间太小&#xff0c;本地网络里有个nas&#xff0c;希望将nas作为数据盘挂载到本地使用。 方法1 基于sftp访问nas 首先nas设置时要打开sftp访问功能。 然后用ubuntu桌面访问服务器的功能登录sftp&#xff0c;类似如下指令 sftp://user192.168.0.100 ubuntu下…

已上传的微信小程序源码丢失,通过反编译找回

前提&#xff1a;你的程序源码已经上传&#xff0c;可以打开体验版或开发版小程序。工具&#xff1a;小程序包解密&#xff1a;链接: https://pan.baidu.com/s/1A2ZCqflr4jMLfg03U_LWHQ 提取码: 4ntn wxappUnpacker&#xff1a;链接: https://pan.baidu.com/s/1HQS8xQsqrhc4hzi…

图纸等敏感文件数据外发时 如何确保效率和安全性?

很多企业随着业务的发展&#xff0c;需要频繁的与外部供应商、合作伙伴之间进行数据的交换和使用。尤其是制造型企业&#xff0c;可能每天都要与几十、上百家供应商及合作伙伴进行产品数据交换。目前&#xff0c;大多数企业已经在内部实施了PDM/PLM系统&#xff0c;实现了对组织…

coco数据集训练nanodet详细流程

github地址 首先要配置环境 conda create -n nanodet python3.8 -y conda activate nanodet确认一下cuda版本 nvcc -V确认是11.3之后&#xff0c;要安装11.3对应的pytorch版本。 本机装pytorch1.12.1后面运行的时候会报错&#xff08;torch没有经过cuda编译&#xff09;&…

C语言中到底是非0表示真,还是1表示真?

我是荔园微风&#xff0c;作为一名在IT界整整25年的老兵&#xff0c;今天我们来重点说一说C语言中到底是非0表示真&#xff0c;还是1表示真&#xff1f;这就是说到C语言中的两个常见运算形式&#xff0c;即关系运算符和逻辑运算符。为照顾急性子的同学&#xff0c;先直接说结论…

【概念辨析】数组指针指针数组

目录 一、数组指针 二、指针数组 三、 数组指针的数组名不是二级指针 再来说最关键的&#xff1a;数组指针为什么不是二级指针呢&#xff1f; 代码如下&#xff1a; 四、指针数组的数组名是二级指针 在复习&#xff0c;在考试&#xff0c;在焦虑。 又一次学习到了数组指针和指针…

MybatisPlus的注意点

userService是基础于ServiceImpl 而servicfeImpl中要传入一个继承于BaseMapper的类和一个实体类 这个继承于BaseMapper的类就是我们的userMapper&#xff0c;所以userMapper要集成BaseMapper 重点来了&#xff0c; UserService中的我们去调用Impl实现类的saveOrUpdate方法&am…

神经网络中超参数调节和优化技巧、优化算法的分类介绍

目录什么是卷积神经网络超参数的调整学习率迭代次数批次大小激活函数隐含层的数目和单元数权重初始化Dropout方法网格搜索和随机搜索神经网络优化算法介绍优化算法分类一阶优化梯度下降算法二阶优化梯度下降算法随机梯度下降(SDG)小批量梯度下降进一步优化梯度下降算法动量方法…

Could not connect to Redis at 127.0.0.1:6379: 由于目标计算机积极拒绝,无法连接。(极简解决办法)

一、遇到问题。 在需要启动Redis客户端的时候&#xff0c;会发现会报这个错误。报这个错误的原因就是Redis的服务端没有开启&#xff0c;那Redis的客户端是访问不了的 二、解决办法。 1.解决的办法就是要启动服务端&#xff0c;让这个客户端可以访问到。启动服务端最简单不会…

秒懂算法 | 基于朴素贝叶斯算法的垃圾信息的识别

本文将带领大家亲手实现一个垃圾信息过滤的算法。 在正式讲解算法之前,最重要的是对整个任务有一个全面的认识,包括算法的输入和输出、可能会用到的技术,以及技术大致的流程。 本任务的目标是去识别一条短信是否为垃圾信息,即输入为一条文本信息,输出为二分类的分类结果。…

百度AI人脸比对

文章目录一、百度智能云1、注册登录2、创建应用3、完成认证领取免费测试资源二、springboot集成1、pom2、配置application.yml3、官方文档三、逻辑分析四、代码1、图片上传2、格式转换3、百度Token获取4、工具类5、实体类6、AI人脸验证7、AI人脸比对8、controller接口一、百度智…

LeetCode010之正则表达式匹配(相关话题:动态规划)

题目描述 给你一个字符串 s 和一个字符规律 p&#xff0c;请你来实现一个支持 . 和 * 的正则表达式匹配。 . 匹配任意单个字符* 匹配零个或多个前面的那一个元素 所谓匹配&#xff0c;是要涵盖 整个 字符串 s的&#xff0c;而不是部分字符串。 示例 1&#xff1a; 输入&…

vue-print-nb使用

下载 pnpm add vue-print-nb --save 全局注册&#xff0c;使用插件的注册方式 或 局部注册自定义指令 import print from vue-print-nb directives: {print } 绑定到点击按钮上 <button v-print"content">Print!</button> 设置配置项-常用 id和popTi…

集群演变( Redis 案例)

文章目录集群演变单节点主从模式哨兵模式Redis Cluster 集群本文浅谈一下集群的发展&#xff0c;用 Redis 做例案例集群演变 集群演变思路 #mermaid-svg-lOtU0w7tegcH7NSB {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermai…

到底是谁还不会写学生管理系统?今天用python来制作一个

前言 咳咳&#xff0c; 想知道还有多少宝子&#xff0c;还在忙毕业设计的事情 准备的怎么样呢&#xff1f; 有些宝子 学python学这么久&#xff0c;还不会自己写出来个学生管理系统 唉&#xff0c;能怎么办呢&#xff0c;我来分享给你们看看呗 完整 代码 点击 领取 即可 话不…

VLAN配置

Vlan的作用 vlan&#xff08;Virtual Local Area Network&#xff09;虚拟局域网 VLAN最大的好处是可以隔离冲突域和广播域 当一个局域网内广播次数过多会影响网络性能 企业越来越大&#xff0c;网络需求发生了变化&#xff0c;内容的终端也成倍增加&#xff0c;流量也增大…

Kotlin 35. Android Gradle 介绍

一起来学Kotlin&#xff1a;概念&#xff1a;22. Android Gradle 介绍 当我们刚开始进行安卓开发的时候&#xff0c;没有人关注 Gradle。我们主要专注于编写 Kotlin 代码和尽可能美观的 Android 应用程序。但随着时间的变化&#xff0c;我自己对 Gradle 感到越来越好奇。到底什…

界面组件Kendo UI for Angular——让网格数据信息显示更全面

Kendo UI致力于新的开发&#xff0c;来满足不断变化的需求&#xff0c;通过React框架的Kendo UI JavaScript封装来支持React Javascript框架。Kendo UI for Angular是专用于Angular开发的专业级Angular组件&#xff0c;telerik致力于提供纯粹的高性能Angular UI组件&#xff0c…

优秀的项目经理需要具备哪些品质呢?

要把项目的任务计划性地分配给各个负责人 1、要明确项目都有负责人&#xff0c;而且许多任务负责人有可能不止一个。用A代表负责人&#xff0c;B代表助手。每项任务都要有一个团队内部的负责人。 2、什么人承担什么责任都需由团队成员共同商讨来决定&#xff0c;这时候就需要…