Spring源码解析-@ComponentScan注解-程序员宅基地

技术标签: spring  java  

Spring死磕系列-@ComponentScan注解

一、ComponentScan注解定义

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Repeatable(ComponentScans.class)
public @interface ComponentScan {
    
    
	/**basePackages属性的别名*/
	@AliasFor("basePackages")
	String[] value() default {
    };
    
	/**指定要扫描的包路径*/
	@AliasFor("value")
	String[] basePackages() default {
    };
    
	/**指定这些类所在的包路径*/
	Class<?>[] basePackageClasses() default {
    };
    
	/**自定义bean名称生成器,给bean创建一个名字*/
	Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;

	/**
	 * The {@link ScopeMetadataResolver} to be used for resolving the scope of detected components.
	 */
	Class<? extends ScopeMetadataResolver> scopeResolver() default AnnotationScopeMetadataResolver.class;

	/**
	 * Indicates whether proxies should be generated for detected components, which may be
	 * necessary when using scopes in a proxy-style fashion.
	 * <p>The default is defer to the default behavior of the component scanner used to
	 * execute the actual scan.
	 * <p>Note that setting this attribute overrides any value set for {@link #scopeResolver}.
	 * @see ClassPathBeanDefinitionScanner#setScopedProxyMode(ScopedProxyMode)
	 */
	ScopedProxyMode scopedProxy() default ScopedProxyMode.DEFAULT;

	/**可以忽略该属性,官方都推荐使用includeFilters和excludeFilters*/
	String resourcePattern() default ClassPathScanningCandidateComponentProvider.DEFAULT_RESOURCE_PATTERN;
    
	/**是否使用默认的过滤器,后面会讲到*/
	boolean useDefaultFilters() default true;

	/**指定include过滤器,满足其中一个才有可能成为候选bean*/
	Filter[] includeFilters() default {
    };

	/**指定exclude过滤器,满足其中一个直接被丢弃*/
	Filter[] excludeFilters() default {
    };

	/**该bean是否需要懒初始化 @since 4.1*/
	boolean lazyInit() default false;

   // 定义过滤器
	@Retention(RetentionPolicy.RUNTIME)
	@Target({
    })
	@interface Filter {
    

		/**指定过滤类型,总共有5种ANNOTATION、ASSIGNABLE_TYPE、ASPECTJ、REGEX、CUSTOM*/
		FilterType type() default FilterType.ANNOTATION;

		/**classes的别名*/
		@AliasFor("classes")
		Class<?>[] value() default {
    };

		/**
		 * The class or classes to use as the filter.
		 * <p>The following table explains how the classes will be interpreted
		 * based on the configured value of the {@link #type} attribute.
		 * <table border="1">
		 * <tr><th>{@code FilterType}</th><th>Class Interpreted As</th></tr>
		 * <tr><td>{@link FilterType#ANNOTATION ANNOTATION}</td>
		 * <td>the annotation itself</td></tr>
		 * <tr><td>{@link FilterType#ASSIGNABLE_TYPE ASSIGNABLE_TYPE}</td>
		 * <td>the type that detected components should be assignable to</td></tr>
		 * <tr><td>{@link FilterType#CUSTOM CUSTOM}</td>
		 * <td>an implementation of {@link TypeFilter}</td></tr>
		 * </table>
		 * <p>When multiple classes are specified, <em>OR</em> logic is applied
		 * &mdash; for example, "include types annotated with {@code @Foo} OR {@code @Bar}".
		 * <p>Custom {@link TypeFilter TypeFilters} may optionally implement any of the
		 * following {@link org.springframework.beans.factory.Aware Aware} interfaces, and
		 * their respective methods will be called prior to {@link TypeFilter#match match}:
		 * <ul>
		 * <li>{@link org.springframework.context.EnvironmentAware EnvironmentAware}</li>
		 * <li>{@link org.springframework.beans.factory.BeanFactoryAware BeanFactoryAware}
		 * <li>{@link org.springframework.beans.factory.BeanClassLoaderAware BeanClassLoaderAware}
		 * <li>{@link org.springframework.context.ResourceLoaderAware ResourceLoaderAware}
		 * </ul>
		 * <p>Specifying zero classes is permitted but will have no effect on component
		 * scanning.
		 * @since 4.2
		 * @see #value
		 * @see #type
		 */
		@AliasFor("value")
		Class<?>[] classes() default {
    };

		/**
		 * The pattern (or patterns) to use for the filter, as an alternative
		 * to specifying a Class {@link #value}.
		 * <p>If {@link #type} is set to {@link FilterType#ASPECTJ ASPECTJ},
		 * this is an AspectJ type pattern expression. If {@link #type} is
		 * set to {@link FilterType#REGEX REGEX}, this is a regex pattern
		 * for the fully-qualified class names to match.
		 * @see #type
		 * @see #classes
		 */
		String[] pattern() default {
    };
	}
}

一、 ComponentScan注解处理

protected final SourceClass doProcessConfigurationClass(
			ConfigurationClass configClass, SourceClass sourceClass, Predicate<String> filter)
			throws IOException {
    

		if (configClass.getMetadata().isAnnotated(Component.class.getName())) {
    
			// Recursively process any member (nested) classes first
			processMemberClasses(configClass, sourceClass, filter);
		}

		// Process any @PropertySource annotations
    	   // 省略处理@PropertySource逻辑
    
		// Process any @ComponentScan annotations
    	// 1. 收集该配置类上所有ComponentScans和ComponentScan注解信息并封装成AnnotationAttributes集合
        Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(
                sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);
    	// 2. 如果该配置类上存在Conditional注解,会进行条件判断是否跳过处理
        if (!componentScans.isEmpty() &&
                !this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {
    
            for (AnnotationAttributes componentScan : componentScans) {
    
                // 3. 进行包扫描,并将结果封装成Set<BeanDefinitionHolder>
                Set<BeanDefinitionHolder> scannedBeanDefinitions =
                        this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());
                // 4. 检查扫描出来的bean是否还有ConfigurationClass,如果有,递归处理
                for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
    
                    BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition();
                    if (bdCand == null) {
    
                        bdCand = holder.getBeanDefinition();
                    }
                    if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) {
    
                        parse(bdCand.getBeanClassName(), holder.getBeanName());
                    }
                }
            }
        }
		// Process any @Import annotations
		   // 省略处理@Import逻辑
		// Process any @ImportResource annotations
		   // 省略处理@ImportResource逻辑
		// Process individual @Bean methods
		   // 省略处理@Bean方法逻辑
		// Process default methods on interfaces
		   // 省略处理接口中定义的default methods
		// Process superclass, if any
		   // 省略处理父类逻辑

		// No superclass -> processing is complete
		return null;
	}
public Set<BeanDefinitionHolder> parse(AnnotationAttributes componentScan, final String declaringClass) {
    
    	// 1. 创建ClassPathBeanDefinitionScanner对象, 该对象能扫描classpath下指定的路径并解析成BeanDefinition
		ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(this.registry,
				componentScan.getBoolean("useDefaultFilters"), this.environment, this.resourceLoader);
		// 2. 下面是对ComponentScan注解属性的解析工作
		Class<? extends BeanNameGenerator> generatorClass = componentScan.getClass("nameGenerator");
		boolean useInheritedGenerator = (BeanNameGenerator.class == generatorClass);
    	// 如果指定了BeanNameGenerator则使用指定的,否则使用默认的AnnotationBeanNameGenerator
		scanner.setBeanNameGenerator(useInheritedGenerator ? this.beanNameGenerator :
				BeanUtils.instantiateClass(generatorClass));

		ScopedProxyMode scopedProxyMode = componentScan.getEnum("scopedProxy");
		if (scopedProxyMode != ScopedProxyMode.DEFAULT) {
    
			scanner.setScopedProxyMode(scopedProxyMode);
		}
		else {
    
			Class<? extends ScopeMetadataResolver> resolverClass = componentScan.getClass("scopeResolver");
			scanner.setScopeMetadataResolver(BeanUtils.instantiateClass(resolverClass));
		}

        // 从@ComponentScan注解属性中解析resourcePattern、includeFilters、
        // excludeFilters、lazyInit并对ClassPathBeanDefinitionScanner做一下基础设置(这部分代码省略自行查看)

		Set<String> basePackages = new LinkedHashSet<>();
    	// 直接指定要扫描的包路径(包括子包)
		String[] basePackagesArray = componentScan.getStringArray("basePackages");
		for (String pkg : basePackagesArray) {
    
            // 解析包路径中存在的占位符
			String[] tokenized = StringUtils.tokenizeToStringArray(this.environment.resolvePlaceholders(pkg),
					ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
			Collections.addAll(basePackages, tokenized);
		}
		for (Class<?> clazz : componentScan.getClassArray("basePackageClasses")) {
    
            // 指定要扫描的类所在包路径(包括子包)
			basePackages.add(ClassUtils.getPackageName(clazz));
		}
    	// 如果没有指定basePackages和basePackageClasses属性,则取@ComponentScan注解所在类的包路径(默认)
		if (basePackages.isEmpty()) {
    
			basePackages.add(ClassUtils.getPackageName(declaringClass));
		}
		// 这个TypeFilter是为了过滤掉@ComponentScan所在的类,因为该类正在被处理
		scanner.addExcludeFilter(new AbstractTypeHierarchyTraversingFilter(false, false) {
    
			@Override
			protected boolean matchClassName(String className) {
    
				return declaringClass.equals(className);
			}
		});
    	// 3. 万事具备,开始扫描每个包路径
		return scanner.doScan(StringUtils.toStringArray(basePackages));
	}

上面代码首先创建了一个ClassPathBeanDefinitionScanner对象,然后解析每个@ComponentScan注解属性并对ClassPathBeanDefinitionScanner对象进行一些基本设置,这些设置会在后面进行包扫描的时候使用。最后通过ClassPathBeanDefinitionScanner#scan方法扫描包路径收集BeanDefinition并返回,你可能会发现返回的是BeanDefinitionHolder集合,其实BeanDefinitionHolder是对BeanDefinition的扩展

BeanDefinitionHolder类

image-20201026233658112

Spring中bean除了有一个BeanDefinition外,每个bean 有一个自己的“大名”,还可以有一堆自己的“小名”,BeanDefinitionHolder只提供了getter方法,所有的属性都是通过构造器注入的。

Scanner类层次结构图

image-20201026235226442

上面是ClassPathBeanDefinitionScanner的类层次结构图,通过上面代码的跟踪以及这两个类的名字我们可以大致猜出这两个类主要是负责扫描包路径下的所有.class文件最后解析封装成BeanDefinition的。牵扯到扫描.class文件就需要资源的加载所以实现了ResourceLoaderAware接口,在解析过程中可能会用到环境变量的解析所以实现了EnviromentCapable接口,下面我们分别看一下这两个类提供了什么功能。

ClassPathScanningCandidateComponentProvider类

image-20201027214631199

我们知道该类是扫描指定包下所有.class文件的,可所有的.class都是我们想要的吗?肯定不是,大多数是我们只想要其中一部分,另一部分不要,因此ClassPathScanningCandidateComponentProvider定义了两个过滤器列表

private final List<TypeFilter> includeFilters = new LinkedList<>();
private final List<TypeFilter> excludeFilters = new LinkedList<>();

这两个成员变量默认是new了一个空的列表,里面内容从哪里来?哪些类我们想要哪些不想要肯定只有我们自己知道,从上面@ComponentScan解析过程中我们知道会解析该注解的includeFilters和excludeFilters属性,其实就是我们在使用该注解时设置的过滤器经过解析最终保存在上面定义的两个列表中,在进行包路径扫描过程中会使用这两个列表进行筛选,将符合要求的封装成BeanDefinition。你这时可能会想到我在使用@ComponentScan注解时也没有指定这两个过滤器呀!(的确是,大多数情况我们是不指定过滤器的)所以当我们没有指定时,贴心的Spring团队给我默认指定了一个。

protected void registerDefaultFilters() {
    
    	// 只要带有@Component注解的就满足
		this.includeFilters.add(new AnnotationTypeFilter(Component.class));
		ClassLoader cl = ClassPathScanningCandidateComponentProvider.class.getClassLoader();
		try {
    
            // 还支持JSR-250提供的@ManagedBean注解
			this.includeFilters.add(new AnnotationTypeFilter(
					((Class<? extends Annotation>) ClassUtils.forName("javax.annotation.ManagedBean", cl)), false));
		}
		catch (ClassNotFoundException ex) {
    
		}
		try {
    
            // 还支持JSR-330提供的@Named注解
			this.includeFilters.add(new AnnotationTypeFilter(
					((Class<? extends Annotation>) ClassUtils.forName("javax.inject.Named", cl)), false));
		}
		catch (ClassNotFoundException ex) {
    
		}
	}

通过上面的分析是不是恍然大悟,原来我们在代码中经常使用的@Controller、@Service、@Repository、@Component被自动扫描由Spring管理了,原来是默认给我们提供了一个过滤器。除此之外,还支持java官方提供的两个注解@ManagedBean和@Named,这个感兴趣可以自己验证。下面我们看看ClassPathScanningCandidateComponentProvider提供的主要方法。

findCandidateComponents方法

public Set<BeanDefinition> findCandidateComponents(String basePackage) {
    
		if (this.componentsIndex != null && indexSupportsIncludeFilters()) {
    
			return addCandidateComponentsFromIndex(this.componentsIndex, basePackage);
		}
		else {
    
			return scanCandidateComponents(basePackage);
		}
	}

scanCandidateComponents方法

private Set<BeanDefinition> scanCandidateComponents(String basePackage) {
    
	Set<BeanDefinition> candidates = new LinkedHashSet<>();
	try {
    
        // 1. 将包路径解析成可加载的类路径
		String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
				resolveBasePackage(basePackage) + '/' + this.resourcePattern;
        // 2. 将每个类文件使用Resource类进行表示
		Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath);
		for (Resource resource : resources) {
    
			if (resource.isReadable()) {
    
				try {
    
                    // 3. 通过Resource对象创建MetadataReader对象
					MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource);
                    // 4. 判断是否是我们需要的候选bean
					if (isCandidateComponent(metadataReader)) {
    
                        // 5. 满足候选bean条件创建ScannedGenericBeanDefinition对象
						ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);
						sbd.setSource(resource);
                        // 6. 再次判断时候是我们需要的候选bean ??? 为啥还要判断
						if (isCandidateComponent(sbd)) {
    
                            // 7. 经过各种难关,过关斩将,最终成为真正符合要求的候选bean
							candidates.add(sbd);
						}
					}
				}
			}
		}
	}
    // 8. 最终返回BeanDefinition列表
	return candidates;
}

看了上面的代码很多人可能和我一样的困惑,为啥要进行两次判断,为了解开谜团,我们还是的进方法一探究竟。先看第4步的判断条件

isCandidateComponent方法1

protected boolean isCandidateComponent(MetadataReader metadataReader) throws IOException {
    
		// 终于瞅见了我们聊了很长时间的过滤器了
		for (TypeFilter tf : this.excludeFilters) {
    
			if (tf.match(metadataReader, getMetadataReaderFactory())) {
    
				return false;
			}
		}
		for (TypeFilter tf : this.includeFilters) {
    
			if (tf.match(metadataReader, getMetadataReaderFactory())) {
    
                // 注意:这块还要进行Condition的判断
				return isConditionMatch(metadataReader);
			}
		}
		return false;
	}

我们终于瞅见了前面说的两个过滤列表了,可以知道内部的过滤策略是:先走excludeFilters如果存在不满足的就直接返回false不往下走了,经过所有的excludeFilters的筛选没有被淘汰,这还没完,还要再经过includeFilters的筛选,要是被includeFilters选中,最后还要通过Condition的条件,满足上面所有条件才能算真正的候选bean(这么一看,成为一个候选bean也不容易)。大致总结一下,没有被excludeFilters淘汰且要同时满足includeFilters和Condition条件。
上面才是第4步判断的条件,我们再看看第6步又做了什么判断

isCandidateComponent方法2

protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
    
		AnnotationMetadata metadata = beanDefinition.getMetadata();
		return (metadata.isIndependent() && (metadata.isConcrete() ||
				(metadata.isAbstract() && metadata.hasAnnotatedMethods(Lookup.class.getName()))));
	}

可以看出要么满足或运算的左边,要么满足或运算的右边。左边:顶级类或者静态内部类且都是具体的实现类(非抽象类或接口),右边:是抽象类且必须有@Lookup注释的方法。

通过上面的分析我们已经知道Scanner是如何经过各种筛选,最终筛选出我们想要的候选bean的。下面我们瞅瞅它的子类ClassPathBeanDefinitionScanner对父类做了哪些扩展。

ClassPathBeanDefinitionScanner类

image-20201027213202668

老套路,还是根据构造器和成员变量猜,然后通过读代码进行验证。我们已经知道父类可以将包路径下面的.class经过层层筛选,选择出候选bean封装成BeanDefinition返回。在类层次结构时说过,这两个类主要是获取候选bean的,现在发现活都让父类干完了,那子类能干点什么呢?通过构造器我们发现都有BeanDefinitionRegistry这个对象,这个对象提供了给容器中增删改查BeanDefinition的功能,所以我们猜测它主要就是将父类返回的BeanDefinition列表注册到容器中,而且还有一个BeanNameGenerator对象,如果不注册bean,要这个对象也没有啥用。下面我们就看一下里面的主要方法。

doScan方法

protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
    
		Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<>();
		for (String basePackage : basePackages) {
    
            // 1. 调用父类方法获取获选bean
			Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
			for (BeanDefinition candidate : candidates) {
    
                // 2. 解析bean的scope并设置
				ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);
				candidate.setScope(scopeMetadata.getScopeName());
				String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);
				if (candidate instanceof AbstractBeanDefinition) {
    
                    // 3. 如果是AbstractBeanDefinition实例,进行一些处理
					postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName);
				}
				if (candidate instanceof AnnotatedBeanDefinition) {
    
                    // 4. 如果是AnnotatedBeanDefinition实例,进行一些处理
					AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);
				}
                // 5. 检查是否该bean已经被注册过了,通过判断容器中是否已经存在该beanName
				if (checkCandidate(beanName, candidate)) {
    
                    // 没有注册过的BeanDefinition才会走到这,并将其封装成BeanDefinitionHolder对象
					BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
                    // 6. 
					definitionHolder =
							AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
					beanDefinitions.add(definitionHolder);
                    // 7. 向容器中注册bean
					registerBeanDefinition(definitionHolder, this.registry);
				}
			}
		}
    	// 8. 返回封装的BeanDefinitionHolder列表
		return beanDefinitions;
	}

postProcessBeanDefinition方法(第3步)

protected void postProcessBeanDefinition(AbstractBeanDefinition beanDefinition, String beanName) {
    
    	// 给BeanDefinition一些默认的设置
		beanDefinition.applyDefaults(this.beanDefinitionDefaults);
		if (this.autowireCandidatePatterns != null) {
    
			beanDefinition.setAutowireCandidate(PatternMatchUtils.simpleMatch(this.autowireCandidatePatterns, beanName));
		}
	}

BeanDefinitionDefaults类定义

image-20201027212734830

AnnotationConfigUtils#processCommonDefinitionAnnotations(第4步)

public static void processCommonDefinitionAnnotations(AnnotatedBeanDefinition abd) {
    
		processCommonDefinitionAnnotations(abd, abd.getMetadata());
	}

调用重载的方法

static void processCommonDefinitionAnnotations(AnnotatedBeanDefinition abd, AnnotatedTypeMetadata metadata) {
    
		AnnotationAttributes lazy = attributesFor(metadata, Lazy.class);
		if (lazy != null) {
    
			abd.setLazyInit(lazy.getBoolean("value"));
		}
		else if (abd.getMetadata() != metadata) {
    
			lazy = attributesFor(abd.getMetadata(), Lazy.class);
			if (lazy != null) {
    
				abd.setLazyInit(lazy.getBoolean("value"));
			}
		}
		if (metadata.isAnnotated(Primary.class.getName())) {
    
			abd.setPrimary(true);
		}
		AnnotationAttributes dependsOn = attributesFor(metadata, DependsOn.class);
		if (dependsOn != null) {
    
			abd.setDependsOn(dependsOn.getStringArray("value"));
		}
		AnnotationAttributes role = attributesFor(metadata, Role.class);
		if (role != null) {
    
			abd.setRole(role.getNumber("value").intValue());
		}
		AnnotationAttributes description = attributesFor(metadata, Description.class);
		if (description != null) {
    
			abd.setDescription(description.getString("value"));
		}
	}

我们发现就是对可能在bean上出现的所有注解@Lazy、@Primary、@DependsOn、@Role、@Description进行解析,如果存在就对该BeanDefinition进行相应的设置

checkCandidate(第5步)

protected boolean checkCandidate(String beanName, BeanDefinition beanDefinition) throws IllegalStateException {
    
    	// 检查容器中是否已经注册过该BeanDefinition,如果没有,则直接返回
    	// 注意:这块是通过bean的“大名”来判断是否注册过
		if (!this.registry.containsBeanDefinition(beanName)) {
    
			return true;
		}
    	// 走到这,说明容器中已经注册过该BeanDefinition,获取注册过的BeanDefinition
		BeanDefinition existingDef = this.registry.getBeanDefinition(beanName);
		BeanDefinition originatingDef = existingDef.getOriginatingBeanDefinition();
		if (originatingDef != null) {
    
			existingDef = originatingDef;
		}
    	// 判断相同名字的两个bean是否兼容,如果不兼容会抛一个异常
		if (isCompatible(beanDefinition, existingDef)) {
    
			return false;
		}
		throw new ConflictingBeanDefinitionException("Annotation-specified bean name '" + beanName +
				"' for bean class [" + beanDefinition.getBeanClassName() + "] conflicts with existing, " +
				"non-compatible bean definition of same name and class [" + existingDef.getBeanClassName() + "]");
	}

applyScopedProxyMode(第6步)

先欠这 这块还没有搞明白 和代理有关

registerBeanDefinition(第7步)

public static void registerBeanDefinition(
			BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry)
			throws BeanDefinitionStoreException {
    
		// 通过“大名”注册BeanDefinition
		String beanName = definitionHolder.getBeanName();
		registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition());
		// 而且还注册bean的“小名”
		String[] aliases = definitionHolder.getAliases();
		if (aliases != null) {
    
			for (String alias : aliases) {
    
				registry.registerAlias(beanName, alias);
			}
		}
	}

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_36118179/article/details/109347621

智能推荐

2024最新计算机毕业设计选题大全-程序员宅基地

文章浏览阅读1.6k次,点赞12次,收藏7次。大家好!大四的同学们毕业设计即将开始了,你们做好准备了吗?学长给大家精心整理了最新的计算机毕业设计选题,希望能为你们提供帮助。如果在选题过程中有任何疑问,都可以随时问我,我会尽力帮助大家。在选择毕业设计选题时,有几个要点需要考虑。首先,选题应与计算机专业密切相关,并且符合当前行业的发展趋势。选择与专业紧密结合的选题,可以使你们更好地运用所学知识,并为未来的职业发展奠定基础。要考虑选题的实际可行性和创新性。选题应具备一定的实践意义和应用前景,能够解决实际问题或改善现有技术。

dcn网络与公网_电信运营商DCN网络的演变与规划方法(The evolution and plan method of DCN)...-程序员宅基地

文章浏览阅读3.4k次。摘要:随着电信业务的发展和电信企业经营方式的转变,DCN网络的定位发生了重大的演变。本文基于这种变化,重点讨论DCN网络的规划方法和运维管理方法。Digest: With the development oftelecommunication bussiness and the change of management of telecomcarrier , DCN’s role will cha..._电信dcn

动手深度学习矩阵求导_向量变元是什么-程序员宅基地

文章浏览阅读442次。深度学习一部分矩阵求导知识的搬运总结_向量变元是什么

月薪已炒到15w?真心建议大家冲一冲数据新兴领域,人才缺口极大!-程序员宅基地

文章浏览阅读8次。近期,裁员的公司越来越多今天想和大家聊聊职场人的新出路。作为席卷全球的新概念ESG已然成为当前各个行业关注的最热风口目前,国内官方发布了一项ESG新证书含金量五颗星、中文ESG证书、完整ESG考试体系、名师主讲...而ESG又是与人力资源直接相关甚至在行业圈内成为大佬们的热门话题...当前行业下行,裁员的公司也越来越多大家还是冲一冲这个新兴领域01 ESG为什么重要?在双碳的大背景下,ESG已然成...

对比传统运营模式,为什么越拉越多的企业选择上云?_系统上云的前后对比-程序员宅基地

文章浏览阅读356次。云计算快速渗透到众多的行业,使中小企业受益于技术变革。最近微软SMB的一项研究发现,到今年年底,78%的中小企业将以某种方式使用云。企业希望投入少、收益高,来取得更大的发展机会。云计算将中小企业信息化的成本大幅降低,它们不必再建本地互联网基础设施,节省时间和资金,降低了企业经营风险。科技创新已成时代的潮流,中小企业上云是创新前提。云平台稳定、安全、便捷的IT环境,提升企业经营效率的同时,也为企业..._系统上云的前后对比

esxi网卡直通后虚拟机无网_esxi虚拟机无法联网-程序员宅基地

文章浏览阅读899次。出现选网卡的时候无法选中,这里应该是一个bug。3.保存退出,重启虚拟机即可。1.先随便选择一个网卡。2.勾先取消再重新勾选。_esxi虚拟机无法联网

随便推点

在LaTeX中使用.bib文件统一管理参考文献_egbib-程序员宅基地

文章浏览阅读913次。在LaTeX中,可在.tex文件的同一级目录下创建egbib.bib文件,所有的参考文件信息可以统一写在egbib.bib文件中,然后在.tex文件的\end{document}前加入如下几行代码:{\small\bibliographystyle{IEEEtran}\bibliography{egbib}}即可在文章中用~\cite{}宏命令便捷的插入文内引用,且文章的Reference部分会自动排序、编号。..._egbib

Unity Shader - Predefined Shader preprocessor macros 着色器预处理宏-程序员宅基地

文章浏览阅读950次。目录:Unity Shader - 知识点目录(先占位,后续持续更新)原文:Predefined Shader preprocessor macros版本:2019.1Predefined Shader preprocessor macros着色器预处理宏Unity 编译 shader programs 期间的一些预处理宏。(本篇的宏介绍随便看看就好,要想深入了解,还是直接看Unity...

大数据平台,从“治理”数据谈起-程序员宅基地

文章浏览阅读195次。本文目录:一、大数据时代还需要数据治理吗?二、如何面向用户开展大数据治理?三、面向用户的自服务大数据治理架构四、总结一、大数据时代还需要数据治理吗?数据平台发展过程中随处可见的数据问题大数据不是凭空而来,1981年第一个数据仓库诞生,到现在已经有了近40年的历史,相对数据仓库来说我还是个年轻人。而国内企业数据平台的建设大概从90年代末就开始了,从第一代架构出现到..._数据治理从0搭建

大学抢课python脚本_用彪悍的Python写了一个自动选课的脚本 | 学步园-程序员宅基地

文章浏览阅读2.2k次,点赞4次,收藏12次。高手请一笑而过。物理实验课别人已经做过3、4个了,自己一个还没做呢。不是咱不想做,而是咱不想起那么早,并且仅有的一次起得早,但是哈工大的服务器竟然超负荷,不停刷新还是不行,不禁感慨这才是真正的“万马争过独木桥“啊!服务器不给力啊……好了,废话少说。其实,我的想法很简单。写一个三重循环,不停地提交,直到所有的数据都accepted。其中最关键的是提交最后一个页面,因为提交用户名和密码后不需要再访问其..._哈尔滨工业大学抢课脚本

english_html_study english html-程序员宅基地

文章浏览阅读4.9k次。一些别人收集的英文站点 http://www.lifeinchina.cn (nice) http://www.huaren.us/ (nice) http://www.hindu.com (okay) http://www.italki.com www.talkdatalk.com (transfer)http://www.en8848.com.cn/yingyu/index._study english html

Cortex-M3双堆栈MSP和PSP_stm32 msp psp-程序员宅基地

文章浏览阅读5.5k次,点赞19次,收藏78次。什么是栈?在谈M3堆栈之前我们先回忆一下数据结构中的栈。栈是一种先进后出的数据结构(类似于枪支的弹夹,先放入的子弹最后打出,后放入的子弹先打出)。M3内核的堆栈也不例外,也是先进后出的。栈的作用?局部变量内存的开销,函数的调用都离不开栈。了解了栈的概念和基本作用后我们来看M3的双堆栈栈cortex-M3内核使用了双堆栈,即MSP和PSP,这极大的方便了OS的设计。MSP的含义是Main..._stm32 msp psp

推荐文章

热门文章

相关标签