spring源码解析-@Autowired

简介

1.1 什么是@Autowired?
  • @Autowired是Spring框架中一个非常重要的注解,用于实现依赖注入(Dependency Injection, DI)。

  • 它可以用来标注在构造器字段或者方法上,Spring会自动将需要的Bean注入到标注的位置。

  • 它属于Spring框架的核心注解,位于org.springframework.beans.factory.annotation包中。

1.2 @Autowired解决了什么问题?

在传统开发中,依赖通常由开发者手动管理,比如通过构造器或Setter方法传递。如下例子:

public class UserService {private UserRepository userRepository;public UserService() {this.userRepository = new UserRepository(); // 手动创建依赖}
}

手动管理依赖有以下几个问题:

  • 耦合性高:UserService与UserRepository紧耦合,不便于扩展或替换。

  • 不易测试:难以对UserRepository进行Mock或动态替换。

  • 灵活性低:代码中硬编码依赖,限制了系统的动态性。 

通过@Autowired注解,可以让依赖注入的工作交由Spring容器管理,代码更加清晰、解耦。

1.3 @Autowired注解的核心功能
  • 自动装配(Autowiring):Spring会根据Bean类型、名称等规则自动将需要的依赖注入到目标对象。

  • 支持多种注入方式

  1. 字段注入:直接注入到类的字段中。

  2. 构造器注入:通过构造函数传递依赖。

  3. Setter方法注入:通过Setter方法完成依赖注入。

  4. 支持多候选Bean的处理:通过@Qualifier、@Primary等解决多个候选Bean冲突的问题。

  5. 处理可选依赖:通过required=false避免没有匹配Bean时导致异常。

1.4 为什么要研究@Autowired的源码
  • 深入理解Spring依赖注入机制:掌握@Autowired底层原理有助于全面理解Spring IOC容器的工作流程

  • 定位和解决问题:源码分析可以帮助开发者在遇到复杂注入问题时快速定位问题根源,例如循环依赖、多实例冲突等。

  • 提升编码能力:通过研究@Autowired,开发者可以学习到Spring框架中优雅的设计思想和编程模式。 

1.5 示例:@Autowired的简单使用
 
@Component
public class UserService {@Autowiredprivate UserRepository userRepository;public void printUserCount() {System.out.println("User count: " + userRepository.count());}
}
  • 以上代码中,@Autowired告诉Spring容器需要为userRepository注入一个UserRepository类型的Bean。

  • Spring通过扫描和依赖注入,确保userRepository在运行时被正确赋值。

2 注入点的扫描

核心类:AutowiredAnnotationBeanPostProcessor.java

核心方法:postProcessMergedBeanDefinition

public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class<?> beanType, String beanName) {// 寻找所有的注入点InjectionMetadata metadata = findAutowiringMetadata(beanName, beanType, null);metadata.checkConfigMembers(beanDefinition);
}private InjectionMetadata findAutowiringMetadata(String beanName, Class<?> clazz, @Nullable PropertyValues pvs) {// Fall back to class name as cache key, for backwards compatibility with custom callers.String cacheKey = (StringUtils.hasLength(beanName) ? beanName : clazz.getName());// Quick check on the concurrent map first, with minimal locking.// 从缓存中获取InjectionMetadata metadata = this.injectionMetadataCache.get(cacheKey);if (InjectionMetadata.needsRefresh(metadata, clazz)) {synchronized (this.injectionMetadataCache) {metadata = this.injectionMetadataCache.get(cacheKey);if (InjectionMetadata.needsRefresh(metadata, clazz)) {if (metadata != null) {metadata.clear(pvs);}// 解析注入点并缓存metadata = buildAutowiringMetadata(clazz);this.injectionMetadataCache.put(cacheKey, metadata);}}}return metadata;
}private InjectionMetadata buildAutowiringMetadata(final Class<?> clazz) {// 如果一个Bean的类型是String...,那么则根本不需要进行依赖注入if (!AnnotationUtils.isCandidateClass(clazz, this.autowiredAnnotationTypes)) {return InjectionMetadata.EMPTY;}List<InjectionMetadata.InjectedElement> elements = new ArrayList<>();Class<?> targetClass = clazz;do {final List<InjectionMetadata.InjectedElement> currElements = new ArrayList<>();// 遍历targetClass中的所有FieldReflectionUtils.doWithLocalFields(targetClass, field -> {// field上是否存在@Autowired、@Value、@Inject中的其中一个MergedAnnotation<?> ann = findAutowiredAnnotation(field);if (ann != null) {// static filed不是注入点,不会进行自动注入if (Modifier.isStatic(field.getModifiers())) {if (logger.isInfoEnabled()) {logger.info("Autowired annotation is not supported on static fields: " + field);}return;}// 构造注入点boolean required = determineRequiredStatus(ann);currElements.add(new AutowiredFieldElement(field, required));}});// 遍历targetClass中的所有MethodReflectionUtils.doWithLocalMethods(targetClass, method -> {Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);if (!BridgeMethodResolver.isVisibilityBridgeMethodPair(method, bridgedMethod)) {return;}// method上是否存在@Autowired、@Value、@Inject中的其中一个MergedAnnotation<?> ann = findAutowiredAnnotation(bridgedMethod);if (ann != null && method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) {// static method不是注入点,不会进行自动注入if (Modifier.isStatic(method.getModifiers())) {if (logger.isInfoEnabled()) {logger.info("Autowired annotation is not supported on static methods: " + method);}return;}// set方法最好有入参if (method.getParameterCount() == 0) {if (logger.isInfoEnabled()) {logger.info("Autowired annotation should only be used on methods with parameters: " +method);}}boolean required = determineRequiredStatus(ann);PropertyDescriptor pd = BeanUtils.findPropertyForMethod(bridgedMethod, clazz);currElements.add(new AutowiredMethodElement(method, required, pd));}});elements.addAll(0, currElements);targetClass = targetClass.getSuperclass();}while (targetClass != null && targetClass != Object.class);return InjectionMetadata.forElements(elements, clazz);
}

3 属性注入

核心类:AutowiredAnnotationBeanPostProcessor.java

核心方法:postProcessProperties

public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) {// 找注入点(所有被@Autowired注解了的Field或Method)InjectionMetadata metadata = findAutowiringMetadata(beanName, bean.getClass(), pvs);try {// 执行注入metadata.inject(bean, beanName, pvs);}catch (BeanCreationException ex) {throw ex;}catch (Throwable ex) {throw new BeanCreationException(beanName, "Injection of autowired dependencies failed", ex);}return pvs;
}public void inject(Object target, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {Collection<InjectedElement> checkedElements = this.checkedElements;Collection<InjectedElement> elementsToIterate =(checkedElements != null ? checkedElements : this.injectedElements);if (!elementsToIterate.isEmpty()) {// 遍历每个注入点进行依赖注入for (InjectedElement element : elementsToIterate) {element.inject(target, beanName, pvs);}}
}

3.1 字段注入

核心类:AutowiredFieldElement

核心方法:inject

protected void inject(Object bean, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {Field field = (Field) this.member;Object value;if (this.cached) {// 对于原型Bean,第一次创建的时候,也找注入点,然后进行注入,此时cached为false,注入完了之后cached为true// 第二次创建的时候,先找注入点(此时会拿到缓存好的注入点),也就是AutowiredFieldElement对象,此时cache为true,也就进到此处了// 注入点内并没有缓存被注入的具体Bean对象,而是beanName,这样就能保证注入到不同的原型Bean对象try {value = resolvedCachedArgument(beanName, this.cachedFieldValue);}catch (NoSuchBeanDefinitionException ex) {// Unexpected removal of target bean for cached argument -> re-resolvevalue = resolveFieldValue(field, bean, beanName);}}else {// 根据filed从BeanFactory中查到的匹配的Bean对象value = resolveFieldValue(field, bean, beanName);}// 反射给filed赋值if (value != null) {ReflectionUtils.makeAccessible(field);field.set(bean, value);}
}private Object resolveFieldValue(Field field, Object bean, @Nullable String beanName) {DependencyDescriptor desc = new DependencyDescriptor(field, this.required);desc.setContainingClass(bean.getClass());Set<String> autowiredBeanNames = new LinkedHashSet<>(1);Assert.state(beanFactory != null, "No BeanFactory available");TypeConverter typeConverter = beanFactory.getTypeConverter();Object value;try {//获取需要注入的对象值value = beanFactory.resolveDependency(desc, beanName, autowiredBeanNames, typeConverter);}catch (BeansException ex) {throw new UnsatisfiedDependencyException(null, beanName, new InjectionPoint(field), ex);}synchronized (this) {if (!this.cached) {Object cachedFieldValue = null;if (value != null || this.required) {cachedFieldValue = desc;// 注册一下beanName依赖了autowiredBeanNames,registerDependentBeans(beanName, autowiredBeanNames);if (autowiredBeanNames.size() == 1) {String autowiredBeanName = autowiredBeanNames.iterator().next();if (beanFactory.containsBean(autowiredBeanName) &&beanFactory.isTypeMatch(autowiredBeanName, field.getType())) {// 构造一个ShortcutDependencyDescriptor作为缓存,保存了当前filed所匹配的autowiredBeanName,而不是对应的bean对象(考虑原型bean)cachedFieldValue = new ShortcutDependencyDescriptor(desc, autowiredBeanName, field.getType());}}}this.cachedFieldValue = cachedFieldValue;this.cached = true;}}return value;}
}
3.2 方法注入

核心类:AutowiredMethodElement

核心方法:inject


protected void inject(Object bean, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {// 如果pvs中已经有当前注入点的值了,则跳过注入if (checkPropertySkipping(pvs)) {return;}Method method = (Method) this.member;Object[] arguments;if (this.cached) {try {arguments = resolveCachedArguments(beanName);}catch (NoSuchBeanDefinitionException ex) {// Unexpected removal of target bean for cached argument -> re-resolvearguments = resolveMethodArguments(method, bean, beanName);}}else {//获取需要注入的值arguments = resolveMethodArguments(method, bean, beanName);}if (arguments != null) {try {//通过反射赋值ReflectionUtils.makeAccessible(method);method.invoke(bean, arguments);}catch (InvocationTargetException ex) {throw ex.getTargetException();}}
}private Object[] resolveMethodArguments(Method method, Object bean, @Nullable String beanName) {int argumentCount = method.getParameterCount();Object[] arguments = new Object[argumentCount];DependencyDescriptor[] descriptors = new DependencyDescriptor[argumentCount];Set<String> autowiredBeans = new LinkedHashSet<>(argumentCount);Assert.state(beanFactory != null, "No BeanFactory available");TypeConverter typeConverter = beanFactory.getTypeConverter();// 遍历每个方法参数,找到匹配的bean对象for (int i = 0; i < arguments.length; i++) {MethodParameter methodParam = new MethodParameter(method, i);DependencyDescriptor currDesc = new DependencyDescriptor(methodParam, this.required);currDesc.setContainingClass(bean.getClass());descriptors[i] = currDesc;try {//获取需要注入的值Object arg = beanFactory.resolveDependency(currDesc, beanName, autowiredBeans, typeConverter);if (arg == null && !this.required) {arguments = null;break;}arguments[i] = arg;}catch (BeansException ex) {throw new UnsatisfiedDependencyException(null, beanName, new InjectionPoint(methodParam), ex);}}synchronized (this) {if (!this.cached) {if (arguments != null) {DependencyDescriptor[] cachedMethodArguments = Arrays.copyOf(descriptors, arguments.length);registerDependentBeans(beanName, autowiredBeans);if (autowiredBeans.size() == argumentCount) {Iterator<String> it = autowiredBeans.iterator();Class<?>[] paramTypes = method.getParameterTypes();for (int i = 0; i < paramTypes.length; i++) {String autowiredBeanName = it.next();if (beanFactory.containsBean(autowiredBeanName) &&beanFactory.isTypeMatch(autowiredBeanName, paramTypes[i])) {cachedMethodArguments[i] = new ShortcutDependencyDescriptor(descriptors[i], autowiredBeanName, paramTypes[i]);}}}this.cachedMethodArguments = cachedMethodArguments;}else {this.cachedMethodArguments = null;}this.cached = true;}}return arguments;}
}
3.3 获取注入属性值

核心类:DefaultListableBeanFactory.java

核心方法:resolveDependency

public Object resolveDependency(DependencyDescriptor descriptor, @Nullable String requestingBeanName,@Nullable Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException {// 用来获取方法入参名字的(比如 (String name)  获取name)descriptor.initParameterNameDiscovery(getParameterNameDiscoverer());// 所需要的类型是Optionalif (Optional.class == descriptor.getDependencyType()) {return createOptionalDependency(descriptor, requestingBeanName);}// 所需要的的类型是ObjectFactory,或ObjectProviderelse if (ObjectFactory.class == descriptor.getDependencyType() ||ObjectProvider.class == descriptor.getDependencyType()) {return new DependencyObjectProvider(descriptor, requestingBeanName);}else if (javaxInjectProviderClass == descriptor.getDependencyType()) {return new Jsr330Factory().createDependencyProvider(descriptor, requestingBeanName);}else {// 在属性或set方法上使用了@Lazy注解,那么则构造一个代理对象并返回,真正使用该代理对象时才进行类型筛选BeanObject result = getAutowireCandidateResolver().getLazyResolutionProxyIfNecessary(descriptor, requestingBeanName);if (result == null) {// descriptor表示某个属性或某个set方法// requestingBeanName表示正在进行依赖注入的Beanresult = doResolveDependency(descriptor, requestingBeanName, autowiredBeanNames, typeConverter);}return result;}
}public Object doResolveDependency(DependencyDescriptor descriptor, @Nullable String beanName,@Nullable Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException {InjectionPoint previousInjectionPoint = ConstructorResolver.setCurrentInjectionPoint(descriptor);try {// 如果当前descriptor之前做过依赖注入了,则可以直接取shortcut了,相当于缓存Object shortcut = descriptor.resolveShortcut(this);if (shortcut != null) {return shortcut;}Class<?> type = descriptor.getDependencyType();// 获取@Value所指定的值Object value = getAutowireCandidateResolver().getSuggestedValue(descriptor);if (value != null) {if (value instanceof String) {// 占位符填充(${}) 从环境变量中获取属性String strVal = resolveEmbeddedValue((String) value);BeanDefinition bd = (beanName != null && containsBean(beanName) ?getMergedBeanDefinition(beanName) : null);// 解析Spring表达式(#{})value = evaluateBeanDefinitionString(strVal, bd);}// 将value转化为descriptor所对应的类型TypeConverter converter = (typeConverter != null ? typeConverter : getTypeConverter());try {return converter.convertIfNecessary(value, type, descriptor.getTypeDescriptor());}catch (UnsupportedOperationException ex) {// A custom TypeConverter which does not support TypeDescriptor resolution...return (descriptor.getField() != null ?converter.convertIfNecessary(value, type, descriptor.getField()) :converter.convertIfNecessary(value, type, descriptor.getMethodParameter()));}}// 如果descriptor所对应的类型是数组、Map这些,就将descriptor对应的类型所匹配的所有bean方法,不用进一步做筛选了Object multipleBeans = resolveMultipleBeans(descriptor, beanName, autowiredBeanNames, typeConverter);if (multipleBeans != null) {return multipleBeans;}// 找到所有Bean,key是beanName, value有可能是bean对象,有可能是beanClassMap<String, Object> matchingBeans = findAutowireCandidates(beanName, type, descriptor);if (matchingBeans.isEmpty()) {// required为true,抛异常if (isRequired(descriptor)) {raiseNoMatchingBeanFound(type, descriptor.getResolvableType(), descriptor);}return null;}String autowiredBeanName;Object instanceCandidate;if (matchingBeans.size() > 1) {// 根据类型找到了多个Bean,进一步筛选出某一个, @Primary-->优先级最高--->nameautowiredBeanName = determineAutowireCandidate(matchingBeans, descriptor);if (autowiredBeanName == null) {if (isRequired(descriptor) || !indicatesMultipleBeans(type)) {return descriptor.resolveNotUnique(descriptor.getResolvableType(), matchingBeans);}else {// In case of an optional Collection/Map, silently ignore a non-unique case:// possibly it was meant to be an empty collection of multiple regular beans// (before 4.3 in particular when we didn't even look for collection beans).return null;}}instanceCandidate = matchingBeans.get(autowiredBeanName);}else {// We have exactly one match.Map.Entry<String, Object> entry = matchingBeans.entrySet().iterator().next();autowiredBeanName = entry.getKey();instanceCandidate = entry.getValue();}// 记录匹配过的beanNameif (autowiredBeanNames != null) {autowiredBeanNames.add(autowiredBeanName);}// 有可能筛选出来的是某个bean的类型,此处就进行实例化,调用getBeanif (instanceCandidate instanceof Class) {instanceCandidate = descriptor.resolveCandidate(autowiredBeanName, type, this);}Object result = instanceCandidate;if (result instanceof NullBean) {if (isRequired(descriptor)) {raiseNoMatchingBeanFound(type, descriptor.getResolvableType(), descriptor);}result = null;}if (!ClassUtils.isAssignableValue(type, result)) {throw new BeanNotOfRequiredTypeException(autowiredBeanName, type, instanceCandidate.getClass());}return result;}finally {ConstructorResolver.setCurrentInjectionPoint(previousInjectionPoint);}
}

4 总结

@Autowired主要分为两个阶段

  1. 注入点的扫描

    1. 扫描类中所有的方法和属性,是否有@Autowired/@Value等注解,如果有这些注解,那么就算一个注入点,添加到缓存中记录

  2. 属性注入

    1. 首先从缓存中获取所有的注入点,遍历注入点进行属性注入或方法注入,流程几乎一致。

    2. 首先通过类型去容器中获取所有符合要求的bean。

    3. 如果需要注入的类型是map/list等集合类型的bean,直接把所有找到的bean注入。

    4. 将找到的bean进行过滤排序,首先是查看是否有@Primary注解,如果有一个,就直接注入,如果有多个,就报错。然后看是否有@Priority注解,获取优先级最高的bean。最后通过beanName确定唯一的bean。

    5. 获取到bean后执行反射完成属性注入。

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

一文学习开源框架OkHttp

OkHttp 是一个开源项目。它由 Square 开发并维护&#xff0c;是一个现代化、功能强大的网络请求库&#xff0c;主要用于与 RESTful API 交互或执行网络通信操作。它是 Android 和 Java 开发中非常流行的 HTTP 客户端&#xff0c;具有高效、可靠、可扩展的特点。 核心特点 高效…

多目标优化算法:多目标极光优化算法(MOPLO)求解ZDT1、ZDT2、ZDT3、ZDT4、ZDT6,提供完整MATLAB代码

一、极光优化算法 极光优化算法&#xff08;Polar Lights Optimization, PLO&#xff09;是2024年提出的一种新型的元启发式优化算法&#xff0c;它从极光这一自然现象中汲取灵感。极光是由太阳风中的带电粒子在地球磁场的作用下&#xff0c;与地球大气层中的气体分子碰撞而产…

【贪心算法第二弹——2208.将数组和减半的最小操作数】

1.题目解析 题目来源 2208.将数组和减半的最小操作数——力扣 测试用例 2.算法原理(贪心策略) 3.实战代码 class Solution { public:int halveArray(vector<int>& nums) {priority_queue<double> hash;double sum 0.0;for(auto e : nums){hash.push(e);sum …

2024最新python使用yt-dlp

2024最新python使用yt-dlp下载YT视频 1.获取yt的cookie1&#xff09;google浏览器下载Get cookies.txt LOCALLY插件2&#xff09;导出cookie 2.yt-dlp下载[yt-dlp的GitHub地址](https://github.com/yt-dlp/yt-dlp?tabreadme-ov-file)1&#xff09;使用Pycharm(2024.3)进行代码…

深入理解下oracle 11g block组成

深层次说&#xff0c;oracle数据库的最少组成单位应该是块&#xff0c;一般默认情况下&#xff0c;oracle数据库的块大小是8kb&#xff0c;其中存储着我们平常所需的数据。我们在使用过程中&#xff0c;难免会疑问道&#xff1a;“oracle数据块中到底是怎样组成的&#xff0c;平…

2024年12月Gesp七级备考知识点拾遗第一期(图的定义及遍历)

目录 总序言 知识点拾遗​编辑 度数 环 二叉树 图的遍历 深度优先 广度优先 连通与强连通 有什么不同 构成分别至少需要几条边&#xff08;易错题&#xff09;&#xff1f; 无向连通图 有向强连通图 完全图 什么是完全图 无向完全图最少边数 有向完全图最少边…

家庭智慧工程师:如何通过科技提升家居生活质量

在今天的数字化时代&#xff0c;家居生活已经不再只是简单的“住”的地方。随着物联网&#xff08;IoT&#xff09;、人工智能&#xff08;AI&#xff09;以及自动化技术的快速发展&#xff0c;越来越多的家庭开始拥抱智慧家居技术&#xff0c;将他们的家变得更加智能化、便捷和…

图像处理实验报告

实验一 图像处理的MATLAB基础 实验目的&#xff1a;熟悉数字图象处理的基本软件工具和操作 实验内容&#xff1a;Matlab应用复习&#xff0c;矩阵产生、操作&#xff1b;矩阵运算以及字符运算。 1.利用增量产生向量[0,2,4,6,8,10]。 2.利用magic(n)函数产生7维魔鬼矩阵A&am…

SpringBoot+SpringCloud面试题整理附答案

什么是SpringBoot&#xff1f; 1、用来简化spring初始搭建和开发过程使用特定的方式进行配置(properties或者yml文件) 2、创建独立的spring引用程序main方法运行 3、嵌入Tomcat无需部署war包&#xff0c;直接打成jar包nohup java -jar – & 启动就好 4、简化了maven的配置 …

Linux之管道,system V的共享内存,消息队列和信号量

Linux之管道&#xff0c;systemV共享内存和信号量 一.进程间通信1.1进程间通信的目的1.2进程间通信的方式 二.管道2.1管道的概念2.2匿名管道2.3命名管道 三.system V3.1共享内存3.2消息队列3.3信号量 一.进程间通信 在我们之前有关Linux指令的学习时我们使用过“|”这个命令&a…

Figma入门-基本操作制作登录页

Figma入门-基本操作制作登录页 前言 在之前的工作中&#xff0c;大家的原型图都是使用 Axure 制作的&#xff0c;印象中 Figma 一直是个专业设计软件。 最近&#xff0c;很多产品朋友告诉我&#xff0c;很多原型图都开始用Figma制作了&#xff0c;并且很多组件都是内置的&am…

Django实现智能问答助手-数据库方式读取问题和答案

扩展 增加问答数据库&#xff0c;通过 Django Admin 添加问题和答案。实现更复杂的问答逻辑&#xff0c;比如使用自然语言处理&#xff08;NLP&#xff09;库。使用前端框架&#xff08;如 Bootstrap&#xff09;增强用户界面 1.注册模型到 Django Admin&#xff08;admin.py…

SQL注入--文件读写注入--理论

什么是文件读写注入&#xff1f; MySQL中有 读取文件的函数&#xff1a;load_file() 写入文件的函数&#xff1a;Into outfile&#xff08;能写入多行&#xff0c;按格式输出&#xff09;和 into dumpfile&#xff08;只能写入一行且没有输出格式&#xff09; 利用这些函数在S…

《最小生成树算法详解:Kruskal的优雅实现》

前置知识和本篇介绍 前置知识&#xff1a; 数据结构-优先级队列&#xff0c; 数据结构-并查集。 Kruskal算法不需要建图&#xff0c; 因此不会建图的模板也没事。 本篇介绍一最小生成树的概念和Kruskal算法。 有关prim算法&#xff08;另一种最小生成树的算法&#xff09;&am…

云计算-华为HCIA-学习笔记

笔者今年7月底考取了华为云计算方向的HCIE认证&#xff0c;回顾从IA到IE的学习和项目实战&#xff0c;想整合和分享自己的学习历程&#xff0c;欢迎志同道合的朋友们一起讨论&#xff01; 第二章&#xff1a;服务器基础 服务器是什么&#xff1f; 服务器本质上就是个性能超强的…

uniapp接入高德地图

下面代码兼容安卓APP和H5 高德地图官网&#xff1a;我的应用 | 高德控制台 &#xff0c;绑定服务选择《Web端(JS API)》 /utils/map.js 需要设置你自己的key和安全密钥 export function myAMap() {return new Promise(function(resolve, reject) {if (typeof window.onLoadM…

C++:探索AVL树旋转的奥秘

文章目录 前言 AVL树为什么要旋转&#xff1f;一、插入一个值的大概过程1. 插入一个值的大致过程2. 平衡因子更新原则3. 旋转处理的目的 二、左单旋1. 左单旋旋转方式总处理图2. 左单旋具体会遇到的情况3. 左单旋代码总结 三、右单旋1. 右单旋旋转方式总处理图2. 右单旋具体会遇…

文小言1:

✨✨ 欢迎大家来访Srlua的博文&#xff08;づ&#xffe3;3&#xffe3;&#xff09;づ╭❤&#xff5e;✨✨ &#x1f31f;&#x1f31f; 欢迎各位亲爱的读者&#xff0c;感谢你们抽出宝贵的时间来阅读我的文章。 我是Srlua小谢&#xff0c;在这里我会分享我的知识和经验。&am…

uni-app 界面TabBar中间大图标设置的两种方法

一、前言 最近写基于uni-app 写app项目的时候&#xff0c;底部导航栏 中间有一个固定的大图标&#xff0c;并且没有激活状态。这里记录下实现方案。效果如下&#xff08;党组织这个图标&#xff09;&#xff1a; 方法一&#xff1a;midButton的使用 官方文档&#xff1a;ta…

CentOS7(Linux)详细安装教程(图文详解)

一、软件准备 本文CentOS7安装在VMware Workstation虚拟机软件,故安装前请自行安装该软件。VMware Workstation官网链接:VMware Workstation官网地址CentOS7下载地址:centos7镜像 如下是最常使用的版本(任选版本)centos-7.9.2009-isos-x86_64安装包下载_开源镜像站-阿里…