从源码中看@Qualifier注解

news2024/11/29 9:46:36

theme: smartblue

摘要

@Qualifier注解通常和@Autowired注解一起使用,那么首先来看@Autowire是怎么把Bean对象注入到Spring容器中的。

前置-@Autowired注入原理

前置条件:需要读者了解@Autowired是如何将类注入进来的。

深入解析 Spring Framework 中 @Autowired 注解的实现原理

@Qualifier注解的demo

在阅读代码之前,先引用著名作者:江南一点雨-松哥写的demo。

定义一个类B,在JavaConfig中定义了两个关于B的Bean,b1和b2,在A类中注入。

public class B {
    private String name;
}
@Configuration
@ComponentScan
public class JavaConfig {

    @Bean(value = "b1")
    @Qualifier
    B b1() {
        return new B();
    }

    @Bean("b2")
    B b2() {
        return new B();
    }
}
@Component
public class A {
    @Autowired
    @Qualifier
    B b;
}

此时如果不使用@Qualifier注解会报错:

根据错误提示来看,类A需要一个单例的Bean,但是找到了两个Bean,通过前文关于@Autowired注解的解析,我们可以知道@Autowired注解是根据类型找到对应的Bean进行注入,由于Bean-b1和b2都是B类型的,所以如果单独使用@Autowired注解是无法将b1、b2注入的,那么@Qualifier做了什么,使Bean正常注入了呢?请看下文。


@Qualifier实现

doResolveDependency

接上文DefaultListableBeanFactory类doResolveDependency()方法,该方法会解析类中注入的各个Bean的信息,并通过方法匹配到一个或多个Bean。

//获取注入的Bean信息
Object multipleBeans = resolveMultipleBeans(descriptor, beanName, autowiredBeanNames, typeConverter);
if (multipleBeans != null) {
        return multipleBeans;
    }

//匹配Bean
Map<String, Object> matchingBeans = findAutowireCandidates(beanName, type, descriptor);
//当匹配到的Bean Map是一个空时的处理逻辑-抛异常
if (matchingBeans.isEmpty()) {
    if (isRequired(descriptor)) {
        raiseNoMatchingBeanFound(type, descriptor.getResolvableType(), descriptor);
    }
    return null;
}

当匹配到的Bean Map是一个空时,会抛出一个我们比较常见的Exception:

NoSuchBeanDefinitionException

resolveMultipleBeans

Spring Framework中的依赖注入(Dependency Injection)实现逻辑,负责解决多个候选Bean与依赖项之间的关系,特别是处理数组、集合和Map类型的依赖项,这段逻辑会根据不同类型Bean执行不同的处理逻辑,确保正确的候选Bean被注入到依赖项中。

@Nullable
private Object resolveMultipleBeans(DependencyDescriptor descriptor, @Nullable String beanName,
        @Nullable Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) {

    Class<?> type = descriptor.getDependencyType();

    if (descriptor instanceof StreamDependencyDescriptor) {
        // 处理流类型的依赖项
        // 查找匹配的候选Bean
        Map<String, Object> matchingBeans = findAutowireCandidates(beanName, type, descriptor);
        if (autowiredBeanNames != null) {
            autowiredBeanNames.addAll(matchingBeans.keySet());
        }
        Stream<Object> stream = matchingBeans.keySet().stream()
                .map(name -> descriptor.resolveCandidate(name, type, this))
                .filter(bean -> !(bean instanceof NullBean));
        if (((StreamDependencyDescriptor) descriptor).isOrdered()) {
             如果依赖项有排序要求,则对流进行排序
            stream = stream.sorted(adaptOrderComparator(matchingBeans));
        }
        return stream;
    }
    else if (type.isArray()) {
        // 处理数组类型的依赖项
        // 查找匹配的候选Bean
        // 然后将这些候选Bean转换为数组
        // 如果需要,可以根据排序比较器对数组进行排序
        // 返回最终结果
     }
    else if (Collection.class.isAssignableFrom(type) && type.isInterface()) {
        // 处理集合类型的依赖项
        // 查找匹配的候选Bean
        // 然后将这些候选Bean转换为集合
        // 如果需要,可以根据排序比较器对集合进行排序
        // 返回最终结果
     }
     else if (Map.class == type) {
        // 处理Map类型的依赖项
        // 查找匹配的候选Bean
        // 返回匹配的候选Bean
    }
    else {
     // 其他情况,返回null
      return null;
}

    // 其他代码省略
}

findAutowireCandidates

通过注释可以看到,该方法的作用是查找与所需类型匹配的 Bean 实例,结合@Qualifier注解的作用是明确注入的Bean来看,该方法中会存在@Qualifier注解的逻辑处理。

	/**
	 * Find bean instances that match the required type.
	 * Called during autowiring for the specified bean.
	 * @param beanName the name of the bean that is about to be wired
	 * @param requiredType the actual type of bean to look for
	 * (may be an array component type or collection element type)
	 * @param descriptor the descriptor of the dependency to resolve
	 * @return a Map of candidate names and candidate instances that match
	 * the required type (never {@code null})
	 * @throws BeansException in case of errors
	 * @see #autowireByType
	 * @see #autowireConstructor
	 */
	protected Map<String, Object> findAutowireCandidates(
			@Nullable String beanName, Class<?> requiredType, DependencyDescriptor descriptor) {

		String[] candidateNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
				this, requiredType, true, descriptor.isEager());
		Map<String, Object> result = CollectionUtils.newLinkedHashMap(candidateNames.length);

    	//...省略...
        
		for (String candidate : candidateNames) {
			if (!isSelfReference(beanName, candidate) && isAutowireCandidate(candidate, descriptor)) {
				addCandidateEntry(result, candidate, descriptor, requiredType);
			}
		}
		if (result.isEmpty()) {
			boolean multiple = indicatesMultipleBeans(requiredType);
			// Consider fallback matches if the first pass failed to find anything...
			DependencyDescriptor fallbackDescriptor = descriptor.forFallbackMatch();
			for (String candidate : candidateNames) {
				if (!isSelfReference(beanName, candidate) && isAutowireCandidate(candidate, fallbackDescriptor) &&
						(!multiple || getAutowireCandidateResolver().hasQualifier(descriptor))) {
					addCandidateEntry(result, candidate, descriptor, requiredType);
				}
			}
			if (result.isEmpty() && !multiple) {
				// Consider self references as a final pass...
				// but in the case of a dependency collection, not the very same bean itself.
				for (String candidate : candidateNames) {
					if (isSelfReference(beanName, candidate) &&
							(!(descriptor instanceof MultiElementDescriptor) || !beanName.equals(candidate)) &&
							isAutowireCandidate(candidate, fallbackDescriptor)) {
						addCandidateEntry(result, candidate, descriptor, requiredType);
					}
				}
			}
		}
		return result;
	}

重点代码:

String[] candidateNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
        this, requiredType, true, descriptor.isEager());

大家看我的条件断点:

由上文的demo代码可知,我在A类中注入了两个B类型的Bean:b1和b2,那么在Spring启动时,框架本身就会查找候选的依赖关系和Bean,并将Bean注入,所以在此时便会获取到b1、b2。该功能的实现是依赖于:BeanFactoryUtils.beanNamesForTypeIncludingAncestors()

public static String[] beanNamesForTypeIncludingAncestors(
        ListableBeanFactory lbf, Class<?> type, boolean includeNonSingletons, boolean allowEagerInit) {

    Assert.notNull(lbf, "ListableBeanFactory must not be null");
    String[] result = lbf.getBeanNamesForType(type, includeNonSingletons, allowEagerInit);
    if (lbf instanceof HierarchicalBeanFactory) {
        HierarchicalBeanFactory hbf = (HierarchicalBeanFactory) lbf;
        if (hbf.getParentBeanFactory() instanceof ListableBeanFactory) {
            String[] parentResult = beanNamesForTypeIncludingAncestors(
                    (ListableBeanFactory) hbf.getParentBeanFactory(), type, includeNonSingletons, allowEagerInit);
            result = mergeNamesWithParent(result, parentResult, hbf);
        }
    }
    return result;
}
  • ListableBeanFactory:用于从Spring中查找Bean
  • Class<?> type:要查找的bean类型
  • boolean includeNonSingletons: 是否应该包括非单例的bean
  • boolean allowEagerInit:是否应该允许提前初始化bean。

  1. 获取bean名称数组:接下来,代码使用lbf.getBeanNamesForType方法获取与指定类型匹配的bean名称数组,这是通过Spring容器的ListableBeanFactory接口提供的方法。
  2. 检查是否有父级bean工厂:然后,代码检查传入的lbf是否是HierarchicalBeanFactory的实例,如果是,就说明可能存在父级bean工厂。HierarchicalBeanFactory是用于管理bean工厂层次结构的接口。
  3. 递归查找:如果存在父级bean工厂,代码将使用递归调用beanNamesForTypeIncludingAncestors方法来查找祖先bean工厂中与指定类型匹配的bean名称数组,并将结果合并到当前的result数组中。这是通过获取祖先bean工厂并再次调用相同的方法来实现的。
  4. 返回结果:最后,方法返回包含所有匹配的bean名称的result数组,包括可能从祖先bean工厂中继承的名称。

Java中运行多重继承,因此该方法使用了递归查询

请详细看一下这段代码:

DependencyDescriptor fallbackDescriptor = descriptor.forFallbackMatch();
        for (String candidate : candidateNames) {
            if (!isSelfReference(beanName, candidate) && isAutowireCandidate(candidate, fallbackDescriptor) &&
                    (!multiple || getAutowireCandidateResolver().hasQualifier(descriptor))) {
                addCandidateEntry(result, candidate, descriptor, requiredType);
            }
        }
  • for (String candidate : candidateNames): 开始循环每一个给定的Bean
  • if (!isSelfReference(beanName, candidate) && …): 检查当前候选bean是否不与需要自动装配的bean存在自引用关系
  • isAutowireCandidate(candidate, fallbackDescriptor)): 判断当前beanName是否为候选的注入bean
  • multiple为true,检查候选bean是否具有@Qualifier注解: 将满足上述条件的候选bean添加到结果集result中,作为一个有效的自动装配候选bean。

isAutowireCandidate

在执行判断当前beanName是否为候选的注入Bean前,会调用四次isAutowireCandidate方法。

protected boolean isAutowireCandidate(
    String beanName, DependencyDescriptor descriptor, AutowireCandidateResolver resolver)
throws NoSuchBeanDefinitionException {

    String bdName = BeanFactoryUtils.transformedBeanName(beanName);
    if (containsBeanDefinition(bdName)) {
        return isAutowireCandidate(beanName, getMergedLocalBeanDefinition(bdName), descriptor, resolver);
    }
    else if (containsSingleton(beanName)) {
        return isAutowireCandidate(beanName, new RootBeanDefinition(getType(beanName)), descriptor, resolver);
    }

    BeanFactory parent = getParentBeanFactory();
    if (parent instanceof DefaultListableBeanFactory dlbf) {
        // No bean definition found in this factory -> delegate to parent.
        return dlbf.isAutowireCandidate(beanName, descriptor, resolver);
    }
    else if (parent instanceof ConfigurableListableBeanFactory clbf) {
        // If no DefaultListableBeanFactory, can't pass the resolver along.
        return clbf.isAutowireCandidate(beanName, descriptor);
    }
    else {
        return true;
    }
}

最后会到达AutowireCandidateResolver接口,查看接口实现,QualifierAnnotationAutowireCandidateResolver

是@Qualifier注解的处理逻辑。

/**
 * Determine whether the provided bean definition is an autowire candidate.
 * <p>To be considered a candidate the bean's <em>autowire-candidate</em>
 * attribute must not have been set to 'false'. Also, if an annotation on
 * the field or parameter to be autowired is recognized by this bean factory
 * as a <em>qualifier</em>, the bean must 'match' against the annotation as
 * well as any attributes it may contain. The bean definition must contain
 * the same qualifier or match by meta attributes. A "value" attribute will
 * fallback to match against the bean name or an alias if a qualifier or
 * attribute does not match.
 * @see Qualifier
 */
@Override
public boolean isAutowireCandidate(BeanDefinitionHolder bdHolder, DependencyDescriptor descriptor) {
    boolean match = super.isAutowireCandidate(bdHolder, descriptor);
    if (match) {
        match = checkQualifiers(bdHolder, descriptor.getAnnotations());
        if (match) {
            MethodParameter methodParam = descriptor.getMethodParameter();
            if (methodParam != null) {
                Method method = methodParam.getMethod();
                if (method == null || void.class == method.getReturnType()) {
                    match = checkQualifiers(bdHolder, methodParam.getMethodAnnotations());
                }
            }
        }
    }
    return match;
}

第一步首先调用父类的isAutowireCandidate进行校验匹配。

@Override
public boolean isAutowireCandidate(BeanDefinitionHolder bdHolder, DependencyDescriptor descriptor) {
    if (!super.isAutowireCandidate(bdHolder, descriptor)) {
        // If explicitly false, do not proceed with any other checks...
        return false;
    }
    return checkGenericTypeMatch(bdHolder, descriptor);
}

在Debug中查看:

A类中通过@Autowired注解了B类型的Bean b1,因此,通过调用父类的isAutowireCandidate方法返回true,进行下一步判断。

接下来执行checkQualifiers。

Match the given qualifier annotations against the candidate bean definition.

翻译:将给定的注解去匹配所有候选Bean定义,以确定使用哪个Bean进行装配。

在Spring自动装配机制中,当存在多个类型相同的Bean时,自动装配可能会失败,因此Spring无法知道使用哪个Bean,此时,可以使用限定符(@Qualifier)来指定所需要的Bean。

	/**
	 * Match the given qualifier annotations against the candidate bean definition.
	 */
	protected boolean checkQualifiers(BeanDefinitionHolder bdHolder, Annotation[] annotationsToSearch) {
		if (ObjectUtils.isEmpty(annotationsToSearch)) {
			return true;
		}
		SimpleTypeConverter typeConverter = new SimpleTypeConverter();
		for (Annotation annotation : annotationsToSearch) {
			Class<? extends Annotation> type = annotation.annotationType();
			boolean checkMeta = true;
			boolean fallbackToMeta = false;
			if (isQualifier(type)) {
				if (!checkQualifier(bdHolder, annotation, typeConverter)) {
					fallbackToMeta = true;
				}
				else {
					checkMeta = false;
				}
			}
			if (checkMeta) {
				boolean foundMeta = false;
				for (Annotation metaAnn : type.getAnnotations()) {
					Class<? extends Annotation> metaType = metaAnn.annotationType();
					if (isQualifier(metaType)) {
						foundMeta = true;
						// Only accept fallback match if @Qualifier annotation has a value...
						// Otherwise it is just a marker for a custom qualifier annotation.
						if ((fallbackToMeta && ObjectUtils.isEmpty(AnnotationUtils.getValue(metaAnn))) ||
								!checkQualifier(bdHolder, metaAnn, typeConverter)) {
							return false;
						}
					}
				}
				if (fallbackToMeta && !foundMeta) {
					return false;
				}
			}
		}
		return true;
	}

在这段代码中,入参是:

  • BeanDefinitionHolder:封装Bean定义及其对应的名称(String类型)和别名(List类型)
  • Annotation[]:用于存储某个程序元素(如类、方法、字段)上的多个注解实例。

注解是一种元数据,它提供了一种在代码中添加、附加额外信息的方式。通过注解,可以为类、方法、字段等元素添加标记和属性,以便在运行时可以基于这些注解进行一些特定的处理逻辑。

在获取到的注解中轮询,针对@Qualifier注解单独处理。所以,在for循环中会判断注解的类型是否为@Qualifier

/**
 * Checks whether the given annotation type is a recognized qualifier type.
 */
protected boolean isQualifier(Class<? extends Annotation> annotationType) {
for (Class<? extends Annotation> qualifierType : this.qualifierTypes) {
    if (annotationType.equals(qualifierType) || annotationType.isAnnotationPresent(qualifierType)) {
        return true;
    }
}
    return false;
}

当判断为注解是@Qualifier时,开始执行@Qualifier注解最核心的一块逻辑:

	protected boolean checkQualifier(
			BeanDefinitionHolder bdHolder, Annotation annotation, TypeConverter typeConverter) {

		Class<? extends Annotation> type = annotation.annotationType();
		RootBeanDefinition bd = (RootBeanDefinition) bdHolder.getBeanDefinition();

		AutowireCandidateQualifier qualifier = bd.getQualifier(type.getName());
		if (qualifier == null) {
			qualifier = bd.getQualifier(ClassUtils.getShortName(type));
		}
		if (qualifier == null) {
			// First, check annotation on qualified element, if any
			Annotation targetAnnotation = getQualifiedElementAnnotation(bd, type);
			// Then, check annotation on factory method, if applicable
			if (targetAnnotation == null) {
				targetAnnotation = getFactoryMethodAnnotation(bd, type);
			}
			if (targetAnnotation == null) {
				RootBeanDefinition dbd = getResolvedDecoratedDefinition(bd);
				if (dbd != null) {
					targetAnnotation = getFactoryMethodAnnotation(dbd, type);
				}
			}
			if (targetAnnotation == null) {
				BeanFactory beanFactory = getBeanFactory();
				// Look for matching annotation on the target class
				if (beanFactory != null) {
					try {
						Class<?> beanType = beanFactory.getType(bdHolder.getBeanName());
						if (beanType != null) {
							targetAnnotation = AnnotationUtils.getAnnotation(ClassUtils.getUserClass(beanType), type);
						}
					}
					catch (NoSuchBeanDefinitionException ex) {
						// Not the usual case - simply forget about the type check...
					}
				}
				if (targetAnnotation == null && bd.hasBeanClass()) {
					targetAnnotation = AnnotationUtils.getAnnotation(ClassUtils.getUserClass(bd.getBeanClass()), type);
				}
			}
			if (targetAnnotation != null && targetAnnotation.equals(annotation)) {
				return true;
			}
		}

		Map<String, Object> attributes = AnnotationUtils.getAnnotationAttributes(annotation);
		if (attributes.isEmpty() && qualifier == null) {
			// If no attributes, the qualifier must be present
			return false;
		}
		for (Map.Entry<String, Object> entry : attributes.entrySet()) {
			String attributeName = entry.getKey();
			Object expectedValue = entry.getValue();
			Object actualValue = null;
			// Check qualifier first
			if (qualifier != null) {
				actualValue = qualifier.getAttribute(attributeName);
			}
			if (actualValue == null) {
				// Fall back on bean definition attribute
				actualValue = bd.getAttribute(attributeName);
			}
			if (actualValue == null && attributeName.equals(AutowireCandidateQualifier.VALUE_KEY) &&
					expectedValue instanceof String name && bdHolder.matchesName(name)) {
				// Fall back on bean name (or alias) match
				continue;
			}
			if (actualValue == null && qualifier != null) {
				// Fall back on default, but only if the qualifier is present
				actualValue = AnnotationUtils.getDefaultValue(annotation, attributeName);
			}
			if (actualValue != null) {
				actualValue = typeConverter.convertIfNecessary(actualValue, expectedValue.getClass());
			}
			if (!expectedValue.equals(actualValue)) {
				return false;
			}
		}
		return true;
	}

  1. 首先获取元数据的类型,这里拿到的是@Qualifier,如果是自定义注解继承自@Qualifier,则拿到的是自定义注解。
  2. 通过上一步获取的注解全/短路径去搜索@Qualifier注解,如果在RootBeanDefinition中可以获取到注解,则开始执行通过元数据工具类获取元数据属性逻辑。
  3. 如果上一步获取到的结果是null,则通过getFactoryMethodAnnotation()方法获取目标注解,一般到该步骤获取到的数据依然是null。
  4. 获取给定的RootBeanDefinition对象中,通过工厂方法解析后的方法上特定类型的注解,举个例子,在上文demo中通过JavaConfig类注入了Bean,那么此时就会通过该类去获取@Qualifier注解中定义的相关信息。
  5. 如果第四步依然没有找到targetAnnotation,则使用RootBeanDefinition对象通过getResolvedDecoratedDefinition方法获取。
  6. 通过代码注释我们可以清晰的看到,如果第五步无法获取,则要去目标类上去获取。
  7. 如果找到了targetAnnotation且与传进来的入参一致,则说明匹配到了正确的bean。
  8. 如果以上未匹配,则说明A类的B属性上,虽然有 @Qualifier 注解,但是只有该注解,没有任何属性,那么显然匹配不上,直接返回 false到上层,到第九步,都是拿到Annotation对象的情况。
  9. 拿到Annotation对象后,遍历这些属性。
  10. 如果还没有拿到 actualValue,并且 attributeName 是 value,并且 expectedValue 是字符串类型,然后判断 bdHolder.matchesName 中是否包含 expectedValue,这个判断实质上就是查看 bdHolder 中定义的 Bean 名称、别名等,是否和 expectedValue 相等,本文 1.1 小节中的案例,将在这里被比对到然后 continue,这里之所以不急着直接 return,是担心后面还有其他属性不满足,如果后续其他属性都满足条件,那么直接在方法结尾处返回 true 即可。
  11. 如果前面还是没能返回,并且 qualifier 不为空,那么就尝试去获取传入注解的默认值,然后进行比较。
Map<String, Object> attributes = AnnotationUtils.getAnnotationAttributes(annotation);
if (attributes.isEmpty() && qualifier == null) {
    // If no attributes, the qualifier must be present
    return false;
}
for (Map.Entry<String, Object> entry : attributes.entrySet()) {
    String attributeName = entry.getKey();
    Object expectedValue = entry.getValue();
    ...
}

引用江南一点雨大佬的一段总结:

以上就是 checkQualifier 方法完整的比较流程。总结一下,其实就两步:

  • 先去找目标类上是否也存在 @Qualifier 注解,就是前面 7 步找 targetAnnotation 的过程,如果目标类上也存在该注解,直接做注解的比对即可,就不去管属性了。
  • 如果没有 targetAnnotation,即 @Qualifier 注解只出现在需求的一方(A 类属性上才有),那么就把这个唯一的 @Qualifier 注解的属性拿出来,分别跟 XML 配置、BeanDefinition 属性、BeanName
    等做比较,如果比对上了,就返回 true。

作者:江南一点雨 链接:https://juejin.cn/post/7266789280336543803 来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

✅ ✅ ✅通过以上流程,通过findAutowireCandidates方法获取到的matchingBeans候选Bean的Map就只有一个满足条件数据,即通过@Qualifier注解实现多个同一类型的Bean明确注入到Spring容器池中。

关于我

👋🏻你好,我是Debug.c。微信公众号:种颗代码技术树 的维护者,一个跨专业自学Java,对技术保持热爱的bug猿,同样也是在某二线城市打拼四年余的Java Coder。

🏆在掘金、CSDN、公众号我将分享我最近学习的内容、踩过的坑以及自己对技术的理解。

📞如果您对我感兴趣,请联系我。

若有收获,就点个赞吧

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

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

相关文章

【QT基础入门 控件篇】QLineEdit 基础、高级和样式表使用详解

一、QLineEdit简介 QLineEdit是一个单行文本编辑器&#xff0c;它可以让用户输入和编辑纯文本&#xff0c;也可以设置一些有用的编辑功能&#xff0c;如撤销和重做、剪切和粘贴、拖放等。QLineEdit: 可以根据不同的回显模式&#xff08;echoMode&#xff09;来显示不同的输入内…

pg14-sql基础(三)-分组

分组 SELECT hire_date, COUNT(*) FROM employees GROUP BY hire_date;SELECT extract(year from hire_date), COUNT(*) FROM employees GROUP BY extract(year from hire_date); -- GROUP BY 1;SELECT extract(year from hire_date), department_id, COUNT(*) FROM employees…

springboot 连接西门子plc,读取对应的值,并修改到数据库

springboot 连接西门子plc&#xff0c;读取对应的值&#xff0c;并修改到数据库 需求&#xff1a;服务器连接plc&#xff0c;读取数据&#xff0c;之后写入到数据库&#xff0c;但是要求速度很快&#xff0c;而且plc中命令对应的值是不断变化的&#xff0c;这个变化&#xff0c…

Python基础入门例程46-NP46 菜品的价格(条件语句)

最近的博文&#xff1a; Python基础入门例程45-NP45 禁止重复注册&#xff08;条件语句&#xff09;-CSDN博客 Python基础入门例程44-NP44 判断列表是否为空&#xff08;条件语句&#xff09;-CSDN博客 Python基础入门例程43-NP43 判断布尔值&#xff08;条件语句&#xff0…

[原创]Cadence17.4,win64系统,构建CIS库

目录 1、背景介绍 2、具体操作流程 3、遇到问题、分析鉴别问题、解决问题 4、借鉴链接并评论 1、背景介绍 CIS库&#xff0c;绘制原理图很方便&#xff0c;但是需要在Cadence软件与数据库之间建立联系&#xff0c;但是一直不成功&#xff0c;花费半天时间才搞明白如何建立关系并…

思维模型 门槛效应/登门槛效应

本系列文章 主要是 分享 思维模型&#xff0c;涉及各个领域&#xff0c;重在提升认知。跨过一个个门槛&#xff0c;走向你该走向的“深渊”和“光明”。 说明&#xff1a;后面 门槛效应/登门槛效应 均使用门槛效应替代 1 门槛效应的应用 1.1 营销策略中的门槛效应 免费试用&…

二维码智慧门牌管理系统升级:一键报警让你的生活更安全!

文章目录 前言一、升级解决方案的特点二、实施步骤 前言 随着科技的不断进步&#xff0c;我们的生活正在逐渐变得更加智能化。可以想象一下&#xff0c;如果你家的门牌也能拥有这种智能升级&#xff0c;将会带来怎样的改变&#xff1f;今天&#xff0c;让我们一起探讨这令人兴…

【Windows】Google和火狐浏览器禁用更新的操作方式

想必很多网民常用的浏览器是Edge&#xff0c;Google&#xff0c;火狐这三种&#xff0c;但是浏览器都有后台自动更新&#xff0c;更新提示会一直显示&#xff0c;要用户去点击才关掉&#xff0c;有点强迫症的用户就会想要把它一直关掉&#xff0c;可每次打开都关不掉&#xff0…

Qt下使用动画框架实现动画520

文章目录 前言一、动画框架的介绍二、示例完整代码三、QtCreator官方示例总结 前言 文章中引用的内容均来自这本书中的原文&#xff1a;【Qt Creator快速入门_霍亚飞编著】。可以通过更改本文示例pro文件中的条件编译&#xff0c;运行示例1和示例2来查看串行动画组和并行动画组…

Qt应用开发--国产工业开发板T113-i的部署教程

Qt在工业上的使用场景包括工业自动化、嵌入式系统、汽车行业、航空航天、医疗设备、制造业和物联网应用。Qt被用来开发工业设备的用户界面、控制系统、嵌入式应用和其他工业应用&#xff0c;因其跨平台性和丰富的功能而备受青睐。 Qt能够为工业领域带来什么好处&#xff1a; - …

基于SSM的网吧计费管理系统(有报告)。Javaee项目,ssm项目。

演示视频&#xff1a; 基于SSM的网吧计费管理系统&#xff08;有报告&#xff09;。Javaee项目&#xff0c;ssm项目。 项目介绍&#xff1a; 采用M&#xff08;model&#xff09;V&#xff08;view&#xff09;C&#xff08;controller&#xff09;三层体系结构&#xff0c;通…

Docker容器技术实战4

11、docker安全 proc未被隔离&#xff0c;所以在容器内和宿主机上看到的东西是一样的 容器资源控制 cpu资源限制 top命令&#xff0c;查看cpu使用率 ctrlpq防止退出回收&#xff0c;容器会直接调用cgroup&#xff0c;自动创建容器id的目录 cpu优先级设定 测试时只保留一个cpu…

Spring的总结

SpringFramework 认识SpringFramework 最首先&#xff0c;我们先要认识Spring和SpringFramework两者之间有什么联系&#xff1f; 广义上的 Spring 泛指以 Spring Framework 为基础的 Spring 技术栈。 狭义的 Spring 特指 Spring Framework&#xff0c;通常我们将它称为 Spr…

Linux多值判断利用case...esac判断

利用这个判断&#xff0c;一定要注意格式的运用&#xff0c;非常容易出错 case $1 in #判断变量的值 "hello") #双引号注意&#xff0c;右括号 echo " afdbab " #语句段&#xff0c;没啥说的 ;; #两个分号结束第一个判断&#xff0c…

python图像处理 ——图像分块

python图像处理 ——图像分块 前言一、分块与合并1.读取原始图像2.网格划分&#xff0c;将图像划分为m*n块3.网格合并 二、代码 前言 根据图像尺寸创建一个 ( m 1 ) ( n 1 ) 个均匀的网格顶点坐标&#xff0c;对于图像块来说每个图像块的左上角和右下角可以唯一确定一个图像…

路由过滤路由引入

目录 一、实验拓扑 二、实验需求 三、实验步骤 1、配置IP地址 2、配置RIP和OSPF 3、配置路由引入 4、使用路由过滤&#xff0c;使 R4 无法学到 R1 的业务网段路由&#xff0c;要求使用 prefix-list 进行匹配 5、OSPF 区域中不能出现 RIP 协议报文 一、实验拓扑 二、实…

【Linux】:Linux项目自动化构建工具——make/Makefile || Linux第一个小程序——进度条(简单版本)

在本章开始给大家分享一个图片 希望对你有帮助 在这里插入图片描述 &#x1f3c6;前言 在开始本章之前 我们需要回顾一下上节课的函数的动静态库的优缺点 动态库的优点&#xff1a; 比较节省资源&#xff08;这里说的资源不仅仅是磁盘资源 也包括网络资源 内存资源等等&#…

SpringBoot项目多环境开发

1.yml文件&#xff08;旧&#xff09; 说明&#xff1a;旧的写法。 #应用环境 spring:profiles:active: dev --- #设置环境#生产环境 spring:profiles: pro server:port: 81--- #开发环境 spirng:profiles: dev server:port: 81--- #测试环境 spring:profiles: test server:p…

修改一下第二次课服务枚举等问题

关于AutoRuns 的总结里面&#xff0c;有个错误&#xff0c;Image hijacks 这个准确的描述应该是镜像劫持 和系统运行相关的image&#xff0c;我们通常指的是二进制镜像文件 Image hijacks镜像劫持 简单来说就是&#xff0c;在注册表中&#xff0c;有部分设置&#xff0c;是规…

阿里云二级域名绑定与宝塔Nginx反向代理配置

在阿里或者腾讯...各大域名商买好域名&#xff0c;备案解析好&#xff0c;目标URL&#xff0c;是真正的地址&#xff0c;比如一些端口&#xff0c;后者会自动填写。 注意ssl配置好&#xff0c;这里不要带反代端口