从源码中看@Qualifier注解


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")@QualifierB b1() {return new B();}@Bean("b2")B b2() {return new B();}
}
@Component
public class A {@Autowired@QualifierB 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) {// 处理流类型的依赖项// 查找匹配的候选BeanMap<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 {// 其他情况,返回nullreturn 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 anyAnnotation targetAnnotation = getQualifiedElementAnnotation(bd, type);// Then, check annotation on factory method, if applicableif (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 classif (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 presentreturn false;}for (Map.Entry<String, Object> entry : attributes.entrySet()) {String attributeName = entry.getKey();Object expectedValue = entry.getValue();Object actualValue = null;// Check qualifier firstif (qualifier != null) {actualValue = qualifier.getAttribute(attributeName);}if (actualValue == null) {// Fall back on bean definition attributeactualValue = 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) matchcontinue;}if (actualValue == null && qualifier != null) {// Fall back on default, but only if the qualifier is presentactualValue = 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 presentreturn 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.hqwc.cn/news/161579.html

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

相关文章

【Vue基础-实践】数据可视化大屏设计(林月明螺蛳粉文化公司单量数据大屏)

目录 一、知识整理 1、页面自适应 2、下载插件px to rem & rpx 3、关于padding与margin 4、下载echarts 5、下载axios 6、experss官网接口创建 7、创建路由 8、api接口创建 9、设置基准路径 10、跨域设置 11、图表设置 12、地图数据引用 13、设置地图效果 二、…

uniapp写一个计算器用于记账(微信小程序,APP)

提要&#xff1a;自己用uniapp写了一个记账小程序&#xff08;目前是小程序&#xff09;&#xff0c;写到计算器部分&#xff0c;在网上找了别人写的计算器&#xff0c;大多数逻辑都是最简单的&#xff0c;都不能满足一个记账计算器的基本逻辑。与其在网上找来找去&#xff0c;…

Leetcode—187.重复的DNA序列【中等】

2023每日刷题&#xff08;二十&#xff09; Leetcode—187.重复的DNA序列 实现代码 class Solution { public:const int L 10;vector<string> findRepeatedDnaSequences(string s) {unordered_map<string, int> str;vector<string> ans;int len s.size()…

vue+vant图片压缩后上传

vuevant图片压缩后上传 vue文件写入 <template><div class"home"><van-field input-align"left"><template #input><van-uploaderv-model"fileList.file":after-read"afterRead":max-count"5":…

SpringBoot + Vue2项目打包部署到服务器后,使用Nginx配置SSL证书,配置访问HTTP协议转HTTPS协议

配置nginx.conf文件&#xff0c;这个文件一般在/etc/nginx/...中&#xff0c;由于每个人的体质不一样&#xff0c;也有可能在别的路径里&#xff0c;自己找找... # 配置工作进程的最大连接数 events {worker_connections 1024; }# 配置HTTP服务 http {# 导入mime.types配置文件…

Python--快速入门二

Python--快速入门二 1.Python数据类型 1.可以通过索引获取字符串中特定位置的字符&#xff1a; a "Hello" print(a[3]) 2.len函数获取字符串的长度&#xff1a; a "Hello" print(a) print(len(a)) 3.空值类型表示完全没有值&#xff1a; 若不确定当…

Spring Data Redis + RabbitMQ - 基于 string + hash 实现缓存,计数(高内聚)

目录 一、Spring Data Redis 1.1、缓存功能(分析) 1.2、案例实现 一、Spring Data Redis 1.1、缓存功能(分析) hash 类型存储缓存相比于 string 类型就有更多的更合适的使用场景. 例如,我有以下这样一个 UserInfo 信息 假设这样一个场景就是:万一只想获取其中某一个…

C++——定义一个 Book(图书)类

完整代码&#xff1a; /*定义一个 Book(图书)类&#xff0c;在该类定义中包括数据成员和成员函数 数据成员&#xff1a;book_name &#xff08;书名&#xff09;、price(价格)和 number(存书数量)&#xff1b; 成员函数&#xff1a;display()显示图书的 情况&#xff1b;borro…

在Node.js中,什么是中间件(middleware)?它们的作用是什么?

聚沙成塔每天进步一点点 ⭐ 专栏简介 前端入门之旅&#xff1a;探索Web开发的奇妙世界 欢迎来到前端入门之旅&#xff01;感兴趣的可以订阅本专栏哦&#xff01;这个专栏是为那些对Web开发感兴趣、刚刚踏入前端领域的朋友们量身打造的。无论你是完全的新手还是有一些基础的开发…

Flutter 小技巧之不一样的思路实现炫酷 3D 翻页折叠动画

今天聊一个比较有意思的 Flutter 动画实现&#xff0c;如果需要实现一个如下图的 3D 折叠动画效果&#xff0c;你会选择通过什么方式&#xff1f; 相信可能很多人第一想法就是&#xff1a;在 Dart 里通过矩阵变换配合 Canvas 实现。 因为这个效果其实也算「常见」&#xff0c;…

[LeetCode]-160. 相交链表-141. 环形链表-142.环形链表II-138.随机链表的复制

目录 160.相交链表 题目 思路 代码 141.环形链表 题目 思路 代码 142.环形链表II 题目 思路 代码 160.相交链表 160. 相交链表 - 力扣&#xff08;LeetCode&#xff09;https://leetcode.cn/problems/intersection-of-two-linked-lists/description/ 题目 给你两个…

Unity中Shader的GI的间接光实现

文章目录 前言一、GI中 间接光照的实现1、看Unity的源码可知&#xff0c;在计算GI的间接光照时&#xff0c;最主要的实现是在UnityGI_Base函数中 二、分析 UnityGI_Base 中实现的功能1、ResetUnityGI的作用2、第一个#if中实现的功能&#xff1a;计算在Distance Shadowmask 中实…