【Spring源码分析】从源码角度去熟悉依赖注入(一)

news2024/10/3 4:40:52

从源码角度去熟悉依赖注入

  • 一、全局出发引出各种依赖注入策略
  • 二、@Autowired依赖注入源码分析
    • 属性注入源码分析(AutowiredFieldElement.inject)
    • 方法注入源码分析(AutowiredMethodElement.inject)
    • 流程图

其实在上篇阐述非懒加载单例Bean的实例化逻辑的时候,就有阐述过 AbstractAutowireCapableBeanFactory#createBean 的大概,它其实就阐述了一个 Bean 的生命周期:

  1. 加载BeanClass;
  2. 实例化前;
  3. 实例化;
  4. 后置处理合并后的 BeanDefinition,MergedBeanDefinitionPostProcessor#postProcessMergedBeanDefinition(这不属于Bean的生命周期,但担任着很重要的角色)
  5. 实例化后;
  6. 属性填充,这里的属性填充其实就是俺们说的依赖注入。
  7. 初始化前、初始化、初始化后。
  8. 硬要加上个结尾的话,就还有个销毁,一般用不着。

先阐述一下,就当上期博客的复习咯。
这期博客呢我们从源码的角度去看看属性填充(依赖注入)的内部实现。
首先阐明一下,像源码分析这种类型的博客呢,要么是自己熟悉原理,要么是自己看过,不然看起来会觉得好困难。

一、全局出发引出各种依赖注入策略

从上篇博客中的流程也可以知道属性填充在处理 MergedBeanDefinitionPostProcessor 之后:

			// 合并后的BeanDefinition的在处理
			applyMergedBeanDefinitionPostProcessors(mbd, beanType, beanName);

			// 属性填充
			populateBean(beanName, mbd, instanceWrapper);

接下来咱从 populateBean() 全局点的角度去引入各种属性注入的方式,下面是该方法的源码:

	@SuppressWarnings("deprecation")  // for postProcessPropertyValues
	protected void populateBean(String beanName, RootBeanDefinition mbd, @Nullable BeanWrapper bw) {

		// Give any InstantiationAwareBeanPostProcessors the opportunity to modify the
		// state of the bean before properties are set. This can be used, for example,
		// to support styles of field injection.
		// 实例化之后,属性设置之前
		// Spring 只是提供了这个生命周期阶段,但是实际上没有做任何处理
		if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
			for (InstantiationAwareBeanPostProcessor bp : getBeanPostProcessorCache().instantiationAware) {
				if (!bp.postProcessAfterInstantiation(bw.getWrappedInstance(), beanName)) {
					return;
				}
			}
		}

		// 这是看 BeanDefinition 中是否已经有填充的属性了
		PropertyValues pvs = (mbd.hasPropertyValues() ? mbd.getPropertyValues() : null);

		// Spring 自己提供的填充策略
		int resolvedAutowireMode = mbd.getResolvedAutowireMode();
		if (resolvedAutowireMode == AUTOWIRE_BY_NAME || resolvedAutowireMode == AUTOWIRE_BY_TYPE) {
			// MutablePropertyValues是PropertyValues具体的实现类
			// 可以看见这里的 newPvs 和上面配置的 PropertyValues 是一起的
			MutablePropertyValues newPvs = new MutablePropertyValues(pvs);
			// Add property values based on autowire by name if applicable.
			if (resolvedAutowireMode == AUTOWIRE_BY_NAME) {
				autowireByName(beanName, mbd, bw, newPvs);
			}
			// Add property values based on autowire by type if applicable.
			if (resolvedAutowireMode == AUTOWIRE_BY_TYPE) {
				autowireByType(beanName, mbd, bw, newPvs);
			}
			pvs = newPvs;
		}


		// 下面是通过注解的方式进行注入的方式
		boolean hasInstAwareBpps = hasInstantiationAwareBeanPostProcessors();
		boolean needsDepCheck = (mbd.getDependencyCheck() != AbstractBeanDefinition.DEPENDENCY_CHECK_NONE);

		PropertyDescriptor[] filteredPds = null;
		if (hasInstAwareBpps) {
			if (pvs == null) {
				pvs = mbd.getPropertyValues();
			}
			for (InstantiationAwareBeanPostProcessor bp : getBeanPostProcessorCache().instantiationAware) {
				// 这里会调用AutowiredAnnotationBeanPostProcessor的postProcessProperties()方法,会直接给对象中的属性赋值
				// AutowiredAnnotationBeanPostProcessor内部并不会处理pvs,直接返回了
				PropertyValues pvsToUse = bp.postProcessProperties(pvs, bw.getWrappedInstance(), beanName);
				if (pvsToUse == null) {
					if (filteredPds == null) {
						filteredPds = filterPropertyDescriptorsForDependencyCheck(bw, mbd.allowCaching);
					}
					pvsToUse = bp.postProcessPropertyValues(pvs, filteredPds, bw.getWrappedInstance(), beanName);
					if (pvsToUse == null) {
						return;
					}
				}
				pvs = pvsToUse;
			}
		}
		if (needsDepCheck) {
			if (filteredPds == null) {
				filteredPds = filterPropertyDescriptorsForDependencyCheck(bw, mbd.allowCaching);
			}
			checkDependencies(beanName, mbd, filteredPds, pvs);
		}

		// 如果当前Bean中的BeanDefinition中设置了PropertyValues,那么最终将是PropertyValues中的值,覆盖@Autowired
		if (pvs != null) {
			applyPropertyValues(beanName, mbd, bw, pvs);
		}
	}
  • 先是处理实例化后,Spring 没实例化后的逻辑,只是提供了这个生命周期阶段;

  • 拿到 BeanDefinition 中已经有填充的属性,就是可以在 MergedBeanDefinitionPostProcessor 中去处理这个 BeanDefinition;比如下面这样自定义:

    • 在这里插入图片描述
  • 在上面拿到的基础上外加上解析是否通过 AUTOWIRE_BY_NAMEAUTOWIRE_BY_TYPE 俩种模式下的填充的属性(PropertyValues);

    • 在这里插入图片描述
  • 通过 InstantiationAwareBeanPostProcessor#postProcessProperties 去填充属性,这里Spring主要是去解析注解填充(常用);

    • 在这里插入图片描述
    • 在这里插入图片描述
  • 最后将需要填充的属性进行个直接引用,通过 InstantiationAwareBeanPostProcessor#postProcessProperties 去填充属性的时候会直接去赋值操作,而最上面俩种是由最后处理,也就是说若俩种方式都使用了,上面的那种才有效,其中上面俩种需要提供对应的 setter 方法。

了解完上面这些就可以开始真正的源码分析环节了,前俩种属性填充其实没啥好说的,这里简单提一下 By_Name 和 By_Type 方式进行的填充源码:
在这里插入图片描述

  • 先是获取到可以注入的属性名(这里面的具体实现是先拿到属性的内省对象,然后遍历去将满足条件的属性返回)
    • 在这里插入图片描述
    • 该属性有对应的set方法;
    • 没有被 excludeFilters 所排除;
    • 先前是没有填充过该属性的;
    • 属性类型不是简单类型
      • 比如下面容器中有个 Number 简单类型的Bean;
      • 在这里插入图片描述
      • 现在尝试注入到UserService中:在这里插入图片描述
      • 可以看见注入失败了在这里插入图片描述
  • 然后遍历返回的属性名,去获取对应的实例,然后放到 PropertyValues 对象中,由后续再直接去调用 setter 去填充。

它是有缺陷的,会把所有满足条件的 setter 方法都当做是去属性注入去调用,而且可以看见它是不让填充简单类型的实例对象的,所以就有了后续的注解式的方式。

二、@Autowired依赖注入源码分析

这里的源码逻辑和 InstantiationAwareBeanPostProcessor#postProcessPropertyValues 里实现的。其主要实现是在 AutowiredAnnotationBeanPostProcessor 中。
有必要去看一下该类的关系结构图:
在这里插入图片描述该 BeanPostProcessor 就一个无参构造,咱来看看,就是俺们希望看见的 @Autowired、@Value 注解。
在这里插入图片描述从上文中也清楚,是先执行的 MergedBeanDefinitionPostProcessor#postProcessMergedBeanDefinition BeanDefinition 的再操作,再去进行属性注入。那我们看这个 BeanPostProcessor 源码,当然也是先去看 postProcessMergedBeanDefinition 方法。接下俩的分析如下:

	@Override
	public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class<?> beanType, String beanName) {
		// 找注入点(所有被@Autowired注解了的Field或Method)
		InjectionMetadata metadata = findAutowiringMetadata(beanName, beanType, null);
		// 将寻找到的注入点放入到 BeanDefinition 中
		metadata.checkConfigMembers(beanDefinition);
	}

findAutowiringMetadata 源码分析:

	private InjectionMetadata findAutowiringMetadata(String beanName, Class<?> clazz, @Nullable PropertyValues pvs) {
		// Fall back to class name as cache key, for backwards compatibility with custom callers.
		// 构造缓存 key,如果beanName不为空,就用beanName,否则用类的全限定名
		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;
	}

也就是说咱得去看这里面的核心代码 buildAutowiringMetadata 它是如何去解析注入点的,拿到元数据然后放到本地缓存中,接下来是看源码环节:

	private InjectionMetadata buildAutowiringMetadata(final Class<?> clazz) {
		// 如果一个Bean的类型是String...,那么则根本不需要进行依赖注入
		// 准确的说是 clazz 的全限定名是以 java. 开头,比如 java.lang.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中的所有Field
			ReflectionUtils.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中的所有Method
			ReflectionUtils.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);
	}
  • 总结流程
    • 从该类开始不断遍历父类
    • 通过反射机制遍历所有的属性
      • 看属性上面是不是有 @Autowired、@Value、@Inject 中的其中一个
      • 排除属性是静态的,静态的属性类方面的成员,不属于实体级别的,不应该进行属性注入
      • 创建注入点 AutowiredFieldElement 实体对象,然后放入到 InjectedElement 集。
    • 通过反射机制遍历所有的方法
      • 其他点和上面属性是一样的,如看方法上有无那些注解,排除方法是静态的…
      • 然后创建注入点 AutowiredMethodElement 实体对象,然后放入到 InjectedElement 集。

OK,也就是通过这咱就拿到了需要注入的属性和方法集,接下来就可以进行注入咯。
现咱去看看 InstantiationAwareBeanPostProcessor#postProcessPropertyValues ,这里指的是 AutowiredAnnotationBeanPostProcessor#postProcessPropertyValues

	@Override
	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;
	}
  • findAutowiringMetadata 方法找到对应需要注入的元数据对象,上面分析过,这里就是从本地缓存拿到的 InjectionMetadata 对象;
  • 接下来直接进行注入~

注入 inject 方法实现其实很简单,就是遍历 InjectedElement 对象,然后依次调用它的 inject 进行遍历。
在这里插入图片描述
咱再过来看看 AutowiredFiledElement、AutowiredMethodElement、InjectedElement 之间的关系:
在这里插入图片描述
接下来就是看属性注入和方法注入是咋地实现的咯~

属性注入源码分析(AutowiredFieldElement.inject)

		@Override
		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-resolve
					value = resolveFieldValue(field, bean, beanName);
				}
			} else {
				// 根据filed从BeanFactory中查到的匹配的Bean对象
				value = resolveFieldValue(field, bean, beanName);
			}

			// 反射给filed赋值
			if (value != null) {
				ReflectionUtils.makeAccessible(field);
				field.set(bean, value);
			}
		}
  • resolveFieldValue 方法,从 BeanFactory 中查询到对应的 Bean 对象;
  • 反射进行属性赋值

方法注入源码分析(AutowiredMethodElement.inject)

		@Override
		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-resolve
					arguments = 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();
				}
			}
		}
  • 排除掉 PropertyValues 已经有过的注入点,因为在上述提到过,是先进行的@Autowired 注入,然后再去 PropertyValues 的注入;(我不知道为啥属性注入那为什么不先排除,再看注不注入)
  • resolveMethodArguments 找各个参数的对象;
  • 反射调用对应的方法。

流程图

这篇已经很长了,现在只是阐述到根据注入点类型找 Bean,如何实现通过注入点类型找 Bean 还没阐述,再写下一篇博客进行阐述,都放这篇可能太长了。

在这里插入图片描述

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

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

相关文章

SpringBoot SaToken Filter如用使用ControllerAdvice统一异常拦截

其实所有的Filter都是一样的原理 大致流程: 创建一个自定义Filter, 用于拦截所有异常此Filter正常进行后续Filter调用当调用后续Filter时, 如果发生异常, 则委托给HandlerExceptionResolver进行后续处理即可 以sa-token的SaServletFilter为例 首先注册SaToken的过滤器 pac…

虚拟化网络

vm1和vm2通过虚拟交换机与主机进行交换&#xff0c; 虚拟交换机&#xff1a;&#xff08;通过软件虚拟出来的交换机&#xff09; 1、LinuxBridge虚拟交换机 2、OVS&#xff08;Open Virtual Switch&#xff09;虚拟交换机 虚拟机的传输是通过虚拟交换机&#xff0c;然后连到…

zabbix监控平台(agent端)

引言&#xff1a;明人不说暗话&#xff0c;上一篇文章我们讲了zabbix的serrver端部署和配置&#xff0c;今天详细讲解一下agent端服务器&#xff08;客户端&#xff09;的配置和关联 1.进入官网 Zabbix&#xff1a;企业级开源监控解决方案 2.进入下载页面选择需要下载的版本信…

时序分解 | Matlab实现SMA-CEEMDAN利用黏菌优化算法优化CEEMDAN时间序列信号分解

时序分解 | Matlab实现SMA-CEEMDAN利用黏菌优化算法优化CEEMDAN时间序列信号分解 目录 时序分解 | Matlab实现SMA-CEEMDAN利用黏菌优化算法优化CEEMDAN时间序列信号分解效果一览基本介绍程序设计参考资料 效果一览 基本介绍 SMA-CEEMDAN利用黏菌优化算法优化CEEMDAN Matlab语言…

C++大学教程(第九版)5.19求Π的值

题目 代码 #include <bits/stdc.h> using namespace std;int main() {double pai 0;for (int count 1, i 1; count < 1000; i 2, count){int flag 1;if (count % 2 0){flag -1;}pai flag * (4.0 / (i * 1.0));cout << "当取前" << co…

前端项目配置 Dockerfile 打包后镜像部署无法访问

Dockerfile 配置如下&#xff1a; FROM node:lts-alpineWORKDIR /app COPY . . RUN npm install RUN npm run buildEXPOSE 3001CMD ["npm", "run", "preview"]构建镜像 docker build -t vite-clarity-project .启动镜像容器 docker run -p 30…

什么牌子的洗地机质量好?洗地机推荐榜

随着时代的进步&#xff0c;清洁家电市场蓬勃发展&#xff0c;懒人经济盛行&#xff0c;许多人对做家务的热情下降。面对繁重的家务活&#xff0c;人们感到无从下手。近年来&#xff0c;洗地机的出现为懒人提供了理想的解决方案。在年货节的氛围中&#xff0c;笔者整理了一些质…

REVIT二次开发生成三维轴网

步骤1 确定轴网 步骤2 生成3D轴网 using System; using System.Collections.Generic; using System.Linq; using System.Text;

【从零开始学习Redis | 第七篇】利用Redis构造全局唯一ID(含其他构造方法)

目录 前言&#xff1a; 什么是全局唯一ID&#xff1f; 尝试构造全局唯一ID&#xff1a; 其他构造全局唯一ID的方法 1.基于数据库自增构造全局唯一ID&#xff1a; 2.基于UUID构造全局唯一ID&#xff1a; 3.基于雪花算法构造全局唯一ID&#xff1a; 总结&#xff1a; 前…

从0开始学前端第三天

学习内容&#xff1a; CSS&#xff1a; JavaScript&#xff1a; 遇到的问题&#xff1a; 1、JavaScript使用console.log无输出&#xff1a; 仔细检查了一下&#xff0c;在script标签中&#xff0c;加入了typemoudle以后&#xff0c;代码连高亮都没有了&#xff0c;应该是没有…

Mysql深度分页优化的一个实践

问题简述: 最近在工作中遇到了大数据量的查询场景, 日产100w左右明细, 会查询近90天内的数据, 总数据量约1亿, 业务要求支持分页查询与导出. 无论是分页或导出都涉及到深度分页查询, mysql通过limit/offset实现的深度分页查询会存在全表扫描的问题, 比如offset1000w, limit10…

elasticsearch 中热词使用遇到的坑

在使用es检索时,一般会创建索引以及索引下mapping和setting一样配置,如下: 命令创建配置方式: PUT /my_index { "settings": { "number_of_shards": 1 }, "mappings": { "properties": { "title": { …

python数字图像处理基础(八)——harris角点检测、图像尺度空间、SIFT算法

目录 harris角点检测原理函数 图像尺度空间概念局部不变性局部不变特征SIFT算法 harris角点检测 原理 Harris 角点检测是一种用于在图像中检测角点的算法。角点是图像中局部区域的交叉点或者突出的特征点。Harris 角点检测算法旨在寻找图像中对于平移、旋转和尺度变化具有不变…

C++设计模式(李建忠)笔记2

C设计模式&#xff08;李建忠&#xff09; 本文是学习笔记&#xff0c;如有侵权&#xff0c;请联系删除。 参考链接 Youtube: C设计模式 Gtihub源码与PPT&#xff1a;https://github.com/ZachL1/Bilibili-plus 豆瓣: 设计模式–可复用面向对象软件的基础 文章目录 C设计模…

vite和webpack的区别和作用

前言 Vite 和 Webpack 都是现代化的前端构建工具&#xff0c;它们可以帮助开发者优化前端项目的构建和性能。虽然它们的目标是相似的&#xff0c;但它们在设计和实现方面有许多不同之处。 一、Vite详解和作用 vite 是什么 vite —— 一个由 vue 作者尤雨溪开发的 web 开发工…

第二百七十三回

文章目录 1. 概念介绍2. 方法与信息2.1 获取方法2.2 详细信息 3. 示例代码4. 内容总结 我们在上一章回中介绍了"蓝牙综合示例"相关的内容&#xff0c;本章回中将介绍如何获取设备信息.闲话休提&#xff0c;让我们一起Talk Flutter吧。 1. 概念介绍 我们在本章回中获…

鸿蒙开发之手势Pan

Entry Component struct OfficialPanGesturePage {State message: string 默认只左右移动State offsetX: number 0State offsetY: number 0State positionX: number 0State positionY: number 0//默认pan的参数&#xff0c;1根手指&#xff0c;左右方向private panOption:…

提纲框架写作方法

论文提纲 论文提纲的意义 有利于检查构思有利于调整修改和写作 拟定提纲的目的 拟标题写总论点做总安排&#xff1a;几个方面&#xff0c;什么顺序做下位论点&#xff1a;每个项目的下位论点&#xff0c;直到段一级&#xff0c;写段的论点句考虑各段安排&#xff0c;把材料…

Visual Studio 与 SQL Server 常见报错解决方案(工作向)

前言 这篇文章从今天创建开始&#xff0c;会一直更新下去&#xff0c;以后遇到常见但是比较容易解决的报错会在本文进行更新&#xff0c;有需要的朋友可以收藏再看 目录 Visual Studio lc.exe已退出&#xff0c;代码为-1无法导入以下密钥文件xxx.pfx&#xff0c;该密钥文件…

RFID涉密文件载体管控系统

1.1 系统简介 RFID涉密文件载体管控系统是一种基于远距离射频识别技术的解决方案&#xff0c;通过非接触式采集射频卡的信息&#xff0c;实现对涉密文件载体的自动识别和监管&#xff0c;该系统集成了计算机软硬件、信息采集处理、数据传输、网络通讯、机械电子、自动控制和智…