SpringBoot运行流程源码分析

news2024/11/26 11:55:34

run方法核心流程

我们在启动SpringBoot的时候调用的是SpringApplication类的静态run方法。其核心流程如下图所示:
在这里插入图片描述
在run方法内完成了SpringApplication的声明周期。,这个过程涉及的几个核心类如下:
SpringApplicationRunListeners:这个接口会在run方法执行的过程中被调用,也就是可以看做SpringApplication的生命周期回调函数,比如ApplicationCntext创建好了可以调用contextPrepared方法,该方法传入的参数是ConfigurableApplicationContext,也就是spring的容器,如果我们有某些业务要在应用启动前进行处理都可以在这个接口的回调中实现。

较新版本的run方法源码如下:

/**
	 * Run the Spring application, creating and refreshing a new
	 * {@link ApplicationContext}.
	 * @param args the application arguments (usually passed from a Java main method)
	 * @return a running {@link ApplicationContext}
	 */
	public ConfigurableApplicationContext run(String... args) {
		Startup startup = Startup.create();
		if (this.registerShutdownHook) {
			SpringApplication.shutdownHook.enableShutdownHookAddition();
		}
		DefaultBootstrapContext bootstrapContext = createBootstrapContext();
		ConfigurableApplicationContext context = null;
		configureHeadlessProperty();
		//从META-INF/spring.factories配置文件中加载Listeners
		SpringApplicationRunListeners listeners = getRunListeners(args);
		//启动listeners的starting回调方法
		listeners.starting(bootstrapContext, this.mainApplicationClass);
		try {
			ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
			//创建环境变量管理类,
			ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
			//打印Springboot的启动画面
			Banner printedBanner = printBanner(environment);
			//创建spring容器
			context = createApplicationContext();
			context.setApplicationStartup(this.applicationStartup);
			//准备spring容器,也就是把环境啥的给spring容器,完成bean定义的加载等功能
			prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
			//刷新spring容器,即开启bean的创建过程(主要是单例bean?)
			refreshContext(context);
			//这个函数目前实现为空,留作后面扩展
			afterRefresh(context, applicationArguments);
			startup.started();
			if (this.logStartupInfo) {
				new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), startup);
			}
			//调用listeners的started回调函数
			listeners.started(context, startup.timeTakenToStarted());
			//调用Runners的实现类,就是在springboot启动后可以实现一些业务处理,比如你的应用可能要升级,做一些升级操作都可以在Runners实现类中完成
			callRunners(context, applicationArguments);
		}
		catch (Throwable ex) {
			throw handleRunFailure(context, ex, listeners);
		}
		try {
			if (context.isRunning()) {
				listeners.ready(context, startup.ready());
			}
		}
		catch (Throwable ex) {
			throw handleRunFailure(context, ex, null);
		}
		return context;
	}

SpringApplicationRunListener监听器

如上所述,这个监听器就是在run方法的执行过程中,会被调用其回调函数的,SpringBoot官方只注册了一个Listener,即EventPublishingRunListener,Listeners都是配置到META-INF/spring.factories配置文件中的。

SpringApplicationRunListener源码

public interface SpringApplicationListener {
	//在上面的run方法里已看到过有调用,在比较前的位置就调用了
	default void starting(){};
	/** environment准备完成,在ApplicationContext创建之前,该方法被调用
	 * Called once the environment has been prepared, but before the
	 * {@link ApplicationContext} has been created.
	 * @param bootstrapContext the bootstrap context
	 * @param environment the environment
	 */
	default void environmentPrepared(ConfigurableBootstrapContext bootstrapContext,
			ConfigurableEnvironment environment) {
	}

	/**
	 * Called once the {@link ApplicationContext} has been created and prepared, but
	 * before sources have been loaded.
	 * @param context the application context
	 */
	default void contextPrepared(ConfigurableApplicationContext context) {
	}

	/**
	 * Called once the application context has been loaded but before it has been
	 * refreshed.
	 * @param context the application context
	 */
	default void contextLoaded(ConfigurableApplicationContext context) {
	}

	/**
	 * The context has been refreshed and the application has started but
	 * {@link CommandLineRunner CommandLineRunners} and {@link ApplicationRunner
	 * ApplicationRunners} have not been called.
	 * @param context the application context.
	 * @param timeTaken the time taken to start the application or {@code null} if unknown
	 * @since 2.6.0
	 */
	default void started(ConfigurableApplicationContext context, Duration timeTaken) {
	}

	/**
	 * Called immediately before the run method finishes, when the application context has
	 * been refreshed and all {@link CommandLineRunner CommandLineRunners} and
	 * {@link ApplicationRunner ApplicationRunners} have been called.
	 * @param context the application context.
	 * @param timeTaken the time taken for the application to be ready or {@code null} if
	 * unknown
	 * @since 2.6.0
	 */
	default void ready(ConfigurableApplicationContext context, Duration timeTaken) {
	}

	/**
	 * Called when a failure occurs when running the application.
	 * @param context the application context or {@code null} if a failure occurred before
	 * the context was created
	 * @param exception the failure
	 * @since 2.0.0
	 */
	default void failed(ConfigurableApplicationContext context, Throwable exception) {
	}
}

官方代码的注释已比较清楚了。总结如下图:
在这里插入图片描述
我们可以自定义SpringApplicationRunListener以实现一些在spring应用启动过程中需要完成的业务逻辑。

初始化ApplicationArguments

这个对象主要用来封装main方法的参数args

初始化ConfigurableEnvironment

ConfiguraableEnvironment接口继承自Environment接口和ConfigurablePropertyResolver接口,主要作用是提供当前运行环境的公开接口,比如配置文件profiles各类系统属性和变量的设置、添加、读取、合并等功能。

源码如下:

public interface ConfigurableEnvironment extends Environment, ConfigurablePropertyResolver {
	//设置激活的组集合
    void setActiveProfiles(String... profiles);
	//向当前激活的组集合中添加一个profile
    void addActiveProfile(String profile);
	//设置默认激活的组集合,激活的组集合为空时会使用默认的组集合
    void setDefaultProfiles(String... profiles);
	//获取当前环境对象中的属性源集合
    MutablePropertySources getPropertySources();
	//获取虚拟机环境变量,该方法提供了直接配置虚拟机环境变量的入口
    Map<String, Object> getSystemProperties();
	//获取操作系统的环境变量
    Map<String, Object> getSystemEnvironment();
	//合并指定环境中的配置到当前环境中
    void merge(ConfigurableEnvironment parent);
}

打印Banner

就是在控制台打印Banner,可以自定义自己要打印的Logo。

Spring应用上下文的创建

应用上下文的创建会根据推断出来的web应用类型,创建不同的应用上下文。springboot中使用如下默认的应用程序上下文工厂创建:

/*
 * Copyright 2012-2022 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.boot;

import java.util.function.BiFunction;
import java.util.function.Supplier;

import org.springframework.aot.AotDetector;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.io.support.SpringFactoriesLoader;

/**
 * Default {@link ApplicationContextFactory} implementation that will create an
 * appropriate context for the {@link WebApplicationType}.
 *
 * @author Phillip Webb
 */
class DefaultApplicationContextFactory implements ApplicationContextFactory {

	@Override
	public Class<? extends ConfigurableEnvironment> getEnvironmentType(WebApplicationType webApplicationType) {
		return getFromSpringFactories(webApplicationType, ApplicationContextFactory::getEnvironmentType, null);
	}

	@Override
	public ConfigurableEnvironment createEnvironment(WebApplicationType webApplicationType) {
		return getFromSpringFactories(webApplicationType, ApplicationContextFactory::createEnvironment, null);
	}

	@Override
	public ConfigurableApplicationContext create(WebApplicationType webApplicationType) {
		try {
			return getFromSpringFactories(webApplicationType, ApplicationContextFactory::create,
					this::createDefaultApplicationContext);
		}
		catch (Exception ex) {
			throw new IllegalStateException("Unable create a default ApplicationContext instance, "
					+ "you may need a custom ApplicationContextFactory", ex);
		}
	}

	private ConfigurableApplicationContext createDefaultApplicationContext() {
		if (!AotDetector.useGeneratedArtifacts()) {
			return new AnnotationConfigApplicationContext();
		}
		return new GenericApplicationContext();
	}

	private <T> T getFromSpringFactories(WebApplicationType webApplicationType,
			BiFunction<ApplicationContextFactory, WebApplicationType, T> action, Supplier<T> defaultResult) {
		for (ApplicationContextFactory candidate : SpringFactoriesLoader.loadFactories(ApplicationContextFactory.class,
				getClass().getClassLoader())) {
			T result = action.apply(candidate, webApplicationType);
			if (result != null) {
				return result;
			}
		}
		return (defaultResult != null) ? defaultResult.get() : null;
	}

}

由源码可以看到需要根据推断出来的应用类型创建上下文对象。

Spring应用上下文的准备

应用上下文准备阶段

主要由三步:
对context设置environment,在AnnotationConfigServletWebServerApplicationContext中的实现如下:

/**
	 * {@inheritDoc}
	 * <p>
	 * Delegates given environment to underlying {@link AnnotatedBeanDefinitionReader} and
	 * {@link ClassPathBeanDefinitionScanner} members.
	 */
	@Override
	public void setEnvironment(ConfigurableEnvironment environment) {
		super.setEnvironment(environment);
		this.reader.setEnvironment(environment);
		this.scanner.setEnvironment(environment);
	}

主要是将环境对象设置给reader和scanner

应用上下文后置处理器:
给上下文对象持有的beanFactory容器设置beanNameGenerator,设置资源加载器和类加载器。ConversionService的设置。

ApplicationContextInitializer初始化context操作:ApplicationContextInitializer接口允许在应用上下文初始化之前(还没有调用refresh刷新应用上下文)执行一些自定义逻辑,需要实现其initialize方法,在这一步会被调用。

应用上下文加载阶段

主要实现对bean定义的加载,在以下方法内:

private void prepareContext(DefaultBootstrapContext bootstrapContext, ConfigurableApplicationContext context,
			ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,
			ApplicationArguments applicationArguments, Banner printedBanner) {
		context.setEnvironment(environment);
		postProcessApplicationContext(context);
		addAotGeneratedInitializerIfNecessary(this.initializers);
		applyInitializers(context);
		listeners.contextPrepared(context);
		bootstrapContext.close(context);
		if (this.logStartupInfo) {
			logStartupInfo(context.getParent() == null);
			logStartupProfileInfo(context);
		}
		// Add boot specific singleton beans
		ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
		beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
		if (printedBanner != null) {
			beanFactory.registerSingleton("springBootBanner", printedBanner);
		}
		if (beanFactory instanceof AbstractAutowireCapableBeanFactory autowireCapableBeanFactory) {
			autowireCapableBeanFactory.setAllowCircularReferences(this.allowCircularReferences);
			if (beanFactory instanceof DefaultListableBeanFactory listableBeanFactory) {
				listableBeanFactory.setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
			}
		}
		if (this.lazyInitialization) {
			context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor());
		}
		if (this.keepAlive) {
			context.addApplicationListener(new KeepAlive());
		}
		context.addBeanFactoryPostProcessor(new PropertySourceOrderingBeanFactoryPostProcessor(context));
		if (!AotDetector.useGeneratedArtifacts()) {
			// Load the sources 加载所有配资源
			Set<Object> sources = getAllSources();
			Assert.notEmpty(sources, "Sources must not be empty");
			//调用加载bean定义的方法
			load(context, sources.toArray(new Object[0]));
		}
		listeners.contextLoaded(context);
	}

这个加载bean定义的核心逻辑在spring源码中,核心实现是在BeanDefinitionLoader中
其构造函数如下:

/**
	 * Create a new {@link BeanDefinitionLoader} that will load beans into the specified
	 * {@link BeanDefinitionRegistry}.
	 * @param registry the bean definition registry that will contain the loaded beans
	 * @param sources the bean sources
	 */
	BeanDefinitionLoader(BeanDefinitionRegistry registry, Object... sources) {
		Assert.notNull(registry, "Registry must not be null");
		Assert.notEmpty(sources, "Sources must not be empty");
		this.sources = sources;
		this.annotatedReader = new AnnotatedBeanDefinitionReader(registry);
		this.xmlReader = new XmlBeanDefinitionReader(registry);
		this.groovyReader = (isGroovyPresent() ? new GroovyBeanDefinitionReader(registry) : null);
		this.scanner = new ClassPathBeanDefinitionScanner(registry);
		this.scanner.addExcludeFilter(new ClassExcludeFilter(sources));
	}

由构造函数可知,这个加载bean定义的类支持多种加载方法,由于目前的sources来源是primarySources配置源和sources配置源,这两个变量的类型分别为Class和String,所以实际上只加载类和字符配置源。我们经常用到的应该是基于java类的注解配置,所以一般是调用下面这个方法:

private void load(Class<?> source) {
		if (isGroovyPresent() && GroovyBeanDefinitionSource.class.isAssignableFrom(source)) {
			// Any GroovyLoaders added in beans{} DSL can contribute beans here
			GroovyBeanDefinitionSource loader = BeanUtils.instantiateClass(source, GroovyBeanDefinitionSource.class);
			((GroovyBeanDefinitionReader) this.groovyReader).beans(loader.getBeans());
		}
		if (isEligible(source)) {
			this.annotatedReader.register(source);
		}
	}

即基于注解的配置类加载过程。

Spring应用上下文的刷新

即调用AbstractApplicationContext的refresh方法,这是spring的功能,在spring-context包内,属于初始化SpringIOC容器的过程。简要代码如下:

public void refresh() throws BeansException, IllegalStateException{
	prepareRefresh();
	
	//通知子类刷新内部bean工厂
	ConfigurableListableBeanFDactory beanFactory = obtainFreshBeanFactory();

	//为当前context准备bean工厂
	prepareBeanFactory(beanFactory);
	try {
		//允许context的子类对bean工厂进行后置处理
		postProcessBeanFactory(beanFactory);
		//调用context中注册为bean的工厂处理器
		invokeBeanFactoryPostProcessors(beanFactory);
		//注册bean处理器(beanPostProcessors)
		registerBeanPostProcessors(beanFactory);

		//初始化context的信息源,和国际化有关
		initMessageSource();
		//初始化context的事件传播器
		initApplicationEventMulticaster();
		//初始化其他特殊的bean
		onRefresh();
		//检查并注册事件监听器
		registerListeners();
		//实例化所有非懒加载单例
		finishBeanFactoryInitialization(beanFactory);
		//最后一步:发布对应事件
		finishRefresh();  
	} catch (BeanException e ) {
		
	} finally {

	}

}

调用ApplicationRunner和CommandLineRunner

即获取上一步中注册在spring容器内的ApplicationRunner和CommandLineRunner实现,对他们进行排序(基于@Order控制执行顺序),然后一个一个调用。所以这两个接口可以在SpringBoot初始化完成之后执行一些处理逻辑。

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

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

相关文章

【C++】:红黑树的应用 --- 封装map和set

点击跳转至文章&#xff1a;【C】&#xff1a;红黑树深度剖析 — 手撕红黑树&#xff01; 目录 前言一&#xff0c;红黑树的改造1. 红黑树的主体框架2. 对红黑树节点结构的改造3. 红黑树的迭代器3.1 迭代器类3.2 Begin() 和 End() 四&#xff0c;红黑树相关接口的改造4.1 Find…

Qt基础 | 自定义界面组件 | 提升法 | 为UI设计器设计自定义界面组件的Widget插件 | MSVC2019编译器中文乱码问题

文章目录 一、自定义 Widget 组件1.自定义 Widget 子类2.自定义 Widget 组件的使用 二、自定义 Qt Designer 插件1.创建 Qt Designer Widget 插件项目2.插件项目各文件的功能实现3.插件的编译与安装4.使用自定义插件5.使用 MSVC 编译器输出中文的问题 一、自定义 Widget 组件 当…

【React】详解受控表单绑定

文章目录 一、受控组件的基本概念1. 什么是受控组件&#xff1f;2. 受控组件的优势3. 基本示例导入和初始化定义函数组件处理输入变化处理表单提交渲染表单导出组件 二、受控组件的进阶用法1. 多个输入框的处理使用多个状态变量使用一个对象管理状态 2. 处理选择框&#xff08;…

leetcode-104. 二叉树的最大深度

题目描述 给定一个二叉树 root &#xff0c;返回其最大深度。 二叉树的 最大深度 是指从根节点到最远叶子节点的最长路径上的节点数。 示例 1&#xff1a; 输入&#xff1a;root [3,9,20,null,null,15,7] 输出&#xff1a;3示例 2&#xff1a; 输入&#xff1a;root [1,n…

24款美规奔驰GLS450更换中规高配主机系统,提升车辆功能和使用体验

平行进口奔驰GLS450 语音小助手要说英语 十分的麻烦 而且也没有导航&#xff0c;原厂记录仪也减少了 很不方便 那要怎么解决呢 往下看 其实很简单&#xff0c;我们只需要更换一台中规的新主机就可以实现以下功能&#xff1a; ①中国地图 ②语音小助手&#xff08;你好&#…

C++编译jsoncpp库

下载https://github.com/hailong0715/jsoncpp/tree/master windows编译工程 jsoncpp-master\makefiles\vs71 1.msvcprtd.lib(MSVCP140D.dll) : error LNK2005 解决办法&#xff1a; (1).工程(Project)->属性(Properties)->配置属性(Configuration Properties)->c/c-…

OZON打开哈萨克斯坦市场,OZON测试开通哈萨克斯坦市场中国产品

在全球化日益深入的今天&#xff0c;跨境电商成为了连接不同国家和地区消费者的重要桥梁。2024年7月26日&#xff0c;Ozon Global宣布了一项重大扩展计划&#xff0c;正式将中国卖家的销售版图拓展至哈萨克斯坦市场&#xff0c;为中国企业打开了新的增长机遇之门。 OZON哈萨克斯…

2024AGI面试官 常问的问题以及答案(附最新的AI大模型算法面试大厂必考100题 )

前言 在这个人工智能飞速发展的时代&#xff0c;AI大模型已经成为各行各业创新与变革的重要驱动力。从自动驾驶、医疗诊断到金融分析&#xff0c;AI大模型的应用场景日益广泛&#xff0c;为我们的生活带来了前所未有的便捷。作为一名程序员&#xff0c;了解并掌握AI大模型的相…

移植QT项目出现无法找到 v143 的生成工具(平台工具集 =“v143”)。若要使用 v143 生成工具进行生成,请安装 v143 生成工具。

由于使用的是visual studio2019&#xff0c;在扩展里没找到msvc v143的工具集&#xff0c;这时候可能需要升级下版本&#xff0c;比如换用visual studio2022 或者在三个地方更改所使用的工具集&#xff0c;一般来讲只要v143编译能通过的v142编译也能通过&#xff0c;所以换用v…

ctfshow-web入门-php特性(web147-web150_plus)

目录 1、web147 2、web148 3、web149 4、web150 5、web150_plus 1、web147 ^&#xff1a;匹配字符串的开头。 $&#xff1a;匹配字符串的结尾&#xff0c;确保整个字符串符合规则。 [a-z0-9_]&#xff1a;表示允许小写字母、数字和下划线。 *&#xff1a;匹配零个或多个前面…

c++入门----类与对象(中)

OK呀&#xff0c;家人们承接上文&#xff0c;当大家看过鄙人的上一篇博客后&#xff0c;我相信大家对我们的c已经有一点印象了。那么我们现在趁热打铁再深入的学习c入门的一些知识。 类的默认成员函数 首先我们学习的是我们的默认函数。不知道大家刚读这个名词是什么反应。默认…

一下午连续故障两次,谁把我们接口堵死了?!

唉。。。 大家好&#xff0c;我是程序员鱼皮。又来跟着鱼皮学习线上事故的处理经验了喔&#xff01; 事故现场 周一下午&#xff0c;我们的 编程导航网站 连续出现了两次故障&#xff0c;每次持续半小时左右&#xff0c;现象是用户无法正常加载网站&#xff0c;一直转圈圈。 …

2020 CSP第一题:数字拆分

2020 CSP第一题&#xff1a;数字拆分 示例1 输入 6 输出 4 2 题意&#xff1a; 实质就是将一个偶数转化为二进制数&#xff0c;然后分别用十进制逆序输出每一项 数据约束&#xff1a; n最大在10的七次方左右&#xff0c;int类型够了&#xff0c;十进制转化为二进制后&#x…

重生之“我打数据结构,真的假的?”--3.栈和队列

1.栈和队列的基本概念 1.1 栈 栈&#xff1a;一种特殊的线性表&#xff0c;其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶&#xff0c;另一端称为栈底。栈中的数据元素遵守后进先出LIFO&#xff08;Last In First Out&#xff09;的原则…

鸿蒙开发——axios封装请求、拦截器

描述&#xff1a;接口用的是PHP&#xff0c;框架TP5 源码地址 链接&#xff1a;https://pan.quark.cn/s/a610610ca406 提取码&#xff1a;rbYX 请求登录 HttpUtil HttpApi 使用方法

开源模型应用落地-LangChain实用小技巧-ChatPromptTemplate的partial方法(一)

一、前言 在当今的自然语言处理领域&#xff0c;LangChain 框架因其强大的功能和灵活性而备受关注。掌握一些实用的小技巧&#xff0c;能够让您在使用 LangChain 框架时更加得心应手&#xff0c;从而更高效地开发出优质的自然语言处理应用。 二、术语 2.1.LangChain 是一个全方…

TCP/IP协议(全的一b)应用层,数据链层,传输层,网络层,以及面试题

目录 TCP/IP协议介绍 协议是什么,有什么作用? 网络协议为什么要分层 TCP/IP五层网络协议每层的作用 应⽤层 DNS的作用及原理 DNS工作流程 数据链路层 以太⽹帧格式 MAC地址的作用 ARP协议的作⽤ ARP协议的工作流程 MTU以及MTU对 IP / UD / TCP 协议的影响 传输层…

MySQL(持续更新中)

第01章_数据库概述 1. 数据库与数据库管理系统 1.1 数据库相关概念 DB&#xff1a;数据库&#xff08;Database&#xff09;即存储数据的“仓库”&#xff0c;其本质是一个文件系统。它保存了一系列有组织的数据DBMS&#xff1a;数据库管理系统&#xff08;Database Manageme…

2024年【广东省安全员B证第四批(项目负责人)】考试报名及广东省安全员B证第四批(项目负责人)模拟考试

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 广东省安全员B证第四批&#xff08;项目负责人&#xff09;考试报名根据新广东省安全员B证第四批&#xff08;项目负责人&#xff09;考试大纲要求&#xff0c;安全生产模拟考试一点通将广东省安全员B证第四批&#x…

AFast and Accurate Dependency Parser using Neural Networks论文笔记

基本信息 作者D Chendoi发表时间2014期刊EMNLP网址https://emnlp2014.org/papers/pdf/EMNLP2014082.pdf 研究背景 1. What’s known 既往研究已证实 传统的dp方法依存句法分析特征向量稀疏&#xff0c;特征向量泛化能力差&#xff0c;特征计算消耗大&#xff0c;并且是人工构…