Spring Boot 启动注解分析
虽然我们在日常开发中,Spring Boot 使用非常多,算是目前 Java 开发领域一个标配了,但是小伙伴们仔细想想自己的面试经历,和 Spring Boot 相关的面试题都有哪些?个人感觉应该是比较少的,Spring Boot 本质上还是曾经 SSM 那一套,只是通过各种 starter 简化了配置而已,其他都是一模一样的,所以 Spring Boot 中很多面试题还是得回归到 Spring 中去解答!当然这并不是说 Spring Boot 中没什么可问的,Spring Boot 中其实也有一个非常经典的面试题,那就是 Spring Boot 中的自动化配置是怎么实现的?今天就来和各位小伙伴聊一下这个问题。
其实之前和小伙伴们聊过相关的问题,不过都是零散的,没有系统梳理过,之前也带领小伙伴们自定义过一个 starter,相信各位小伙伴对于 starter 的原理也有一定了解,所以今天这篇文章一些过于细节的内容我就不赘述了,大家可以翻看之前的文章。
1. @SpringBootApplication
要说 Spring Boot 的自动化配置,那必须从项目的启动类 @SpringBootApplication 说起,这是整个 Spring Boot 宇宙的起点,我们先来看下这个注解:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {}
可以看到,@SpringBootApplication 注解组合了多个常见注解的功能,其中:
- 前四个是元注解,这里我们不做讨论。
- 第五个
@SpringBootConfiguration是一个支持配置类的注解,这里我们也不做讨论。 - 第六个
@EnableAutoConfiguration这个注解就表示开启自动化配置,这是我们今天要聊得重点。 - 第七个
@ComponentScan是一个包扫描注解,为什么Spring Boot项目中的Bean只要放对位置就会被自动扫描到,和这个注解有关。
别看这里注解多,其实真正由 Spring Boot 提供的注解一共就两个,分别是 @SpringBootConfiguration 和 @EnableAutoConfiguration 两个,其他注解在 Spring Boot 出现之前就已经存在多年了。
2. @EnableAutoConfiguration
接下来我们来看看 @EnableAutoConfiguration 是如何实现自动化配置的。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {}
这个注解起关键作用的就是两个东西:
@AutoConfigurationPackage:这个表示自动扫描各种第三方的注解,在之前的文章中已经和大家聊过这个注解的作用了,传送门:@AutoConfigurationPackage 和 @ComponentScan 有何区别?@Import则是在导入AutoConfigurationImportSelector配置类,这个配置类里边就是去加载各种自动化配置类的。
3. AutoConfigurationImportSelector
AutoConfigurationImportSelector 类中的方法比较多,入口的地方则是 process 方法,所以我们这里就从 process 方法开始看起:
@Override
public void process(AnnotationMetadata annotationMetadata, DeferredImportSelector deferredImportSelector) {Assert.state(deferredImportSelector instanceof AutoConfigurationImportSelector,() -> String.format("Only %s implementations are supported, got %s",AutoConfigurationImportSelector.class.getSimpleName(),deferredImportSelector.getClass().getName()));AutoConfigurationEntry autoConfigurationEntry = ((AutoConfigurationImportSelector) deferredImportSelector).getAutoConfigurationEntry(annotationMetadata);this.autoConfigurationEntries.add(autoConfigurationEntry);for (String importClassName : autoConfigurationEntry.getConfigurations()) {this.entries.putIfAbsent(importClassName, annotationMetadata);}
}
从类名就可以看出来,跟自动化配置相关的对象是由 AutoConfigurationEntry autoConfigurationEntry = ((AutoConfigurationImportSelector) deferredImportSelector).getAutoConfigurationEntry(annotationMetadata); 进行加载的。
当然这里的 getAutoConfigurationEntry 方法实际上就是当前类提供的方法,我们来看下该方法:
protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {if (!isEnabled(annotationMetadata)) {return EMPTY_ENTRY;}AnnotationAttributes attributes = getAttributes(annotationMetadata);List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);configurations = removeDuplicates(configurations);Set<String> exclusions = getExclusions(annotationMetadata, attributes);checkExcludedClasses(configurations, exclusions);configurations.removeAll(exclusions);configurations = getConfigurationClassFilter().filter(configurations);fireAutoConfigurationImportEvents(configurations, exclusions);return new AutoConfigurationEntry(configurations, exclusions);
}
这里源码的方法命名都做的不错,基本上都能做到见名知意,小伙伴们日常开发中,应该向这样的命名思路看齐。接下来我们就来挨个看一下这里的关键方法。
3.1 isEnabled
首先调用 isEnabled 方法去判断自动化配置到底有没有开启,这个主要是因为我们及时在项目中引入了 spring-boot-starter-xxx 之后,我们也可以通过在 application.properties 中配置 spring.boot.enableautoconfiguration=false 来关闭所有的自动化配置。
相关源码如下:
protected boolean isEnabled(AnnotationMetadata metadata) {if (getClass() == AutoConfigurationImportSelector.class) {return getEnvironment().getProperty(EnableAutoConfiguration.ENABLED_OVERRIDE_PROPERTY, Boolean.class, true);}return true;
}
3.2 getCandidateConfigurations
接下来调用 getCandidateConfigurations 方法去获取所有候选的自动化配置类,这些候选的自动化配置类主要来自两个地方:
- 在之前的自定义
starter中和大家聊过,我们需要在claspath\:META-INF/spring.factories中定义出来所有的自动化配置类,这是来源一。 Spring Boot自带的自动化配置类,这个在之前的 vhr 视频中也和小伙伴们多次讲过,Spring Boot自带的自动化配置类位于spring-boot-autoconfigure-3.0.6.jar!\META-INF\spring\org.springframework.boot.autoconfigure.AutoConfiguration.imports文件中。
相关源码如下:
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {List<String> configurations = new ArrayList<>(SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader()));ImportCandidates.load(AutoConfiguration.class, getBeanClassLoader()).forEach(configurations::add);Assert.notEmpty(configurations,"No auto configuration classes found in META-INF/spring.factories nor in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. If you "+ "are using a custom packaging, make sure that file is correct.");return configurations;
}
这里加载到的自动化配置类的全路径被存入到 configurations 对象中,该对象有两个获取的地方:
- 调用
SpringFactoriesLoader.loadFactoryNames方法获取,这个方法细节我就不带大家看了,比较简单,本质上就是去加载META-INF/spring.factories文件,这个文件中定义了大量的自动化配置类的全路径。 - 调用
ImportCandidates.load方法去加载,这个就是加载spring-boot-autoconfigure-3.0.6.jar!\META-INF\spring\org.springframework.boot.autoconfigure.AutoConfiguration.imports文件中的自动化配置类。
如果这两个地方都没有加载到任何自动化配置类,那么就会抛出一个异常。
3.3 removeDuplicates
removeDuplicates 方法表示移除候选自动化配置类中重复的类,移除的思路也很有意思,就用一个 LinkedHashSet 中转一下就行了,源码如下:
protected final <T> List<T> removeDuplicates(List<T> list) {return new ArrayList<>(new LinkedHashSet<>(list));
}
可以看到这些源码里有时候一些解决思路也很有意思。
3.4 getExclusions
getExclusions 方法表示需要获取到所有被排除的自动化配置类,这些被排除的自动化配置类可以从三个地方获取:
- 当前注解的
exclude属性。 - 当前注解的
excludeName属性。 application.properties配置文件中的spring.autoconfigure.exclude属性。
来看一下相关源码:
protected Set<String> getExclusions(AnnotationMetadata metadata, AnnotationAttributes attributes) {Set<String> excluded = new LinkedHashSet<>();excluded.addAll(asList(attributes, "exclude"));excluded.addAll(asList(attributes, "excludeName"));excluded.addAll(getExcludeAutoConfigurationsProperty());return excluded;
}
跟上面讲解的三点刚好对应。
3.5 checkExcludedClasses
这个方法是检查所有被排除的自动化配置类,由于 Spring Boot 中的自动化配置类可以自定义,并不需要统一实现某一个接口或者统一继承某一个类,所以在写排除类的时候,如果写错了编译是校验不出来的,像下面这种:
@SpringBootApplication(exclude = HelloController.class)
public class App {public static void main(String[] args) {SpringApplication.run(App.class, args);}
}
由于 HelloController 并不是一个自动化配置类,所以这样写项目启动的时候就会报错,如下:

这个异常从哪来的呢?其实就是来自 checkExcludedClasses 方法,我们来看下该方法:
private void checkExcludedClasses(List<String> configurations, Set<String> exclusions) {List<String> invalidExcludes = new ArrayList<>(exclusions.size());for (String exclusion : exclusions) {if (ClassUtils.isPresent(exclusion, getClass().getClassLoader()) && !configurations.contains(exclusion)) {invalidExcludes.add(exclusion);}}if (!invalidExcludes.isEmpty()) {handleInvalidExcludes(invalidExcludes);}
}
protected void handleInvalidExcludes(List<String> invalidExcludes) {StringBuilder message = new StringBuilder();for (String exclude : invalidExcludes) {message.append("\t- ").append(exclude).append(String.format("%n"));}throw new IllegalStateException(String.format("The following classes could not be excluded because they are not auto-configuration classes:%n%s",message));
}
可以看到,在 checkExcludedClasses 方法中,会首先找到所有位于当前类路径下但是却不包含在 configurations 中的所有被排除的自动化配置类,由于 configurations 中的就是所有的自动化配置类了,所以这些不存在于 configurations 中的类都是有问题的,都不是自动化配置类,将这些有问题的类收集起来,存入到 invalidExcludes 变量中,然后再进行额外的处理。
所谓额外的处理就是在 handleInvalidExcludes 方法中抛出异常,前面截图中的异常就是来自这里。
3.6 removeAll
这个方法就一个任务,就是从 configurations 中移除掉那些被排除的自动化配置类。configurations 本身就是 List 集合,exclusions 则是一个 Set 集合,所以这里直接移除即可。
3.7 filter
现在我们已经加载了所有的自动化配置类了,但是这些配置类并不是都会生效,具体是否生效,还要看你的项目是否使用了具体的依赖。
例如,现在加载的自动化配置里里边就包含了 RedisAutoConfiguration,这个是自动配置 Redis 的,但是由于我的项目中并没有使用 Redis,所以这个自动化配置类并不会生效。这个过程就是由 getConfigurationClassFilter().filter(configurations); 来完成的。
先说一个预备知识:
由于我们项目中的自动化配置类特别多,每一个自动化配置类都会依赖别的类,当别的类存在时,这个自动化配置类才会生效,这一堆互相之间的依赖关系,存在于 spring-boot-autoconfigure-3.0.6.jar!/META-INF/spring-autoconfigure-metadata.properties 文件之中,我随便举一个该文件中的配置:
org.springframework.boot.autoconfigure.amqp.RabbitAnnotationDrivenConfiguration.ConditionalOnClass=org.springframework.amqp.rabbit.annotation.EnableRabbit表示 RabbitAnnotationDrivenConfiguration 类要生效有一个必备条件就是当前项目类路径下要存在org.springframework.amqp.rabbit.annotation.EnableRabbit。
我们来看看 RabbitAnnotationDrivenConfiguration 类的注解:
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(EnableRabbit.class)
class RabbitAnnotationDrivenConfiguration {
}
这个类和配置文件中的内容一致。
这个预备知识搞懂了,接下来的内容就好理解了。
先来看 getConfigurationClassFilter 方法,这个就是获取所有的过滤器,如下:
private ConfigurationClassFilter getConfigurationClassFilter() {if (this.configurationClassFilter == null) {List<AutoConfigurationImportFilter> filters = getAutoConfigurationImportFilters();for (AutoConfigurationImportFilter filter : filters) {invokeAwareMethods(filter);}this.configurationClassFilter = new ConfigurationClassFilter(this.beanClassLoader, filters);}return this.configurationClassFilter;
}
可以看到,这里获取到的过滤器都是 AutoConfigurationImportFilter 类型的,这个类型的过滤器只有三个实例,如下图:

从这三个实例的名字中,基本上就能看出来各自的作用:
- OnClassCondition:这个就是条件注解
@ConditionalOnClass的判定条件,看名字就知道用来判断当前 classpath 下是否存在某个类。 - OnWebApplicationCondition:这个是条件注解
ConditionalOnWebApplication的判定条件,用来判断当前系统环境是否是一个 Web 环境。 - OnBeanCondition:这个是条件注解
@ConditionalOnBean的判定条件,就是判断当前系统下是否存在某个 Bean。
这里获取到的三个 AutoConfigurationImportFilter 过滤器其实就是上面这三个。接下来执行 filter 方法,如下:
List<String> filter(List<String> configurations) {long startTime = System.nanoTime();String[] candidates = StringUtils.toStringArray(configurations);boolean skipped = false;for (AutoConfigurationImportFilter filter : this.filters) {boolean[] match = filter.match(candidates, this.autoConfigurationMetadata);for (int i = 0; i < match.length; i++) {if (!match[i]) {candidates[i] = null;skipped = true;}}}if (!skipped) {return configurations;}List<String> result = new ArrayList<>(candidates.length);for (String candidate : candidates) {if (candidate != null) {result.add(candidate);}}return result;
}
这里就是遍历这三个过滤器,然后分别调用各自的 match 方法和 144 个自动化配置类进行匹配,如果这些自动化配置类所需要的条件得到满足,则 match 数组对应的位置就为 true,否则就为 false。
然后遍历 match 数组,将不满足条件的自动化配置类置为 null,最后再把这些 null 移除掉。
这样就获取到了我们需要进行自动化配置的类了。
最后一句 fireAutoConfigurationImportEvents 则是触发自动化配置类导入事件,这个没啥好说的~
当这些自动化配置类加载进来之后,接下来就是各种条件注解来决定这些配置类是否生效了,这些都比较简单了,之前在 vhr 种也和小伙伴们讲过多次了,这里就不再啰嗦了~
好啦,经过上面的梳理相信小伙伴们对 Spring Boot 自动化配置类的加载有一个大概的认知了吧~
相关文章:
Spring Boot 启动注解分析
虽然我们在日常开发中,Spring Boot 使用非常多,算是目前 Java 开发领域一个标配了,但是小伙伴们仔细想想自己的面试经历,和 Spring Boot 相关的面试题都有哪些?个人感觉应该是比较少的,Spring Boot 本质上还…...
React Native数据存储
最近做RN开发中需要数据存储,查阅RN官方资料,发现推荐我们使用 AsyncStorage,对使用步骤做一下记录。 AsyncStorage是什么 简单的,异步的,持久化的key-value存储系统AsyncStorage在IOS下存储分为两种情况: 存储内容较…...
【网络编程】揭开套接字的神秘面纱
文章目录 1 :peach:简单理解TCP/UDP协议 :peach:2 :peach:网络字节序 :peach:3 :peach:socket编程接口 :peach:3.1 :apple:socket 常见API :apple:3.2 :apple:sockaddr结构:apple: 4 :peach:简单的UDP网络程序 :peach:4.1 :apple:基本分析:apple:4.2 :apple:udpServer.hpp(重点…...
MySQL 8.0 事务定义和基本操作
MySQL 事务(Transaction)的四大特性:A、C、I、D A、原子性:(Atomicity) 一个事务是不可分割的最小工作单位。 执行的事务,要么全部成功,要么回滚到执行事务之前的状态。 C、一致…...
项目经理必备:常用的项目管理系统推荐!
当我们成为项目负责人时,找到合适的工具来管理跟进项目,就成为了迫切需要解决的问题。一款优秀的工具,在项目的管理跟进中,起着极为重要的作用,一般可以付费购买专门的项目管理软件。 1.可快速切换查看不同角度的项目信…...
【香瓜说职场】信任危机(2022.08.19)
自从17年4月份开始辞职创业,已经5年零4个月了。今天跟大家聊一点不太正能量的事。 首先关于“要不要说些不好的”这件事,我爸妈常建议我不要把不好的事情写出来,因为觉得丢人、不体面、怕影响合伙人关系、影响同事关系。而我觉得如果只写好的…...
【Rust】Rust学习 第六章枚举和模式匹配
本章介绍 枚举(enumerations),也被称作 enums。枚举允许你通过列举可能的 成员(variants) 来定义一个类型。首先,我们会定义并使用一个枚举来展示它是如何连同数据一起编码信息的。接下来,我们会…...
Win10安装GPU支持的最新版本的tensorflow
我在安装好cuda和cudnn后,使用pip install tensorflow安装的tensorflow都提示不能找到GPU, 为此怀疑默认暗转的tensorflow是不带GPU支持的。 在tensorflow官网提供了多个版本的GPU支持的windows的安装包 https://www.tensorflow.org/install/pip?hlz…...
20个Golang自动化DevOps库
探索 20 个用于简化任务和提高生产力的重要库。 Golang,也称为 Go,是一种静态类型、编译型编程语言,由 Google 的 Robert Griesemer、Rob Pike 和 Ken Thompson 设计。它于 2009 年推出,旨在解决其他编程语言的缺点,特…...
【WiFi】WiFi 6E最新支持的国家和频段
信道Map图 国家和频段 CountryStatus Spectrum Andorra Adopted Considering 5945-6425 MHz 6425-7125 MHz ArgentinaAdopted5925-7125 MHzAustralia Adopted Considering 5925-6425 MHz 6425-7125 MHz Austria Adopted Considering 5945-6425 MHz 6425-7125 MHz BahrainA…...
如何使用html,包括css,js 画思维导图?有哪些可用的方法?
首先,创建一个新的HTML文件,可以使用任何文本编辑器。在文件中添加必要的标签和结构来定义网页的内容和布局。 <!DOCTYPE html> <html> <head><meta charset"UTF-8"><title>Mind Map</title><link re…...
机器学习---梯度下降代码
1. 归一化 # Read data from csv pga pd.read_csv("pga.csv") print(type(pga))print(pga.head())# Normalize the data 归一化值 (x - mean) / (std) pga.distance (pga.distance - pga.distance.mean()) / pga.distance.std() pga.accuracy (pga.accuracy - pg…...
【VB6|第23期】原来Jet.OLEDB也可以读取新版.xlsx的Excel文件
日期:2023年8月11日 作者:Commas 签名:(ง •_•)ง 积跬步以致千里,积小流以成江海…… 注释:如果您觉得有所帮助,帮忙点个赞,也可以关注我,我们一起成长;如果有不对的地方…...
通过控制ros节点的启停,软实现人工控制和紧急停止功能的图示
通过控制ros节点的启停,软实现人工控制和紧急停止功能的图示 实现原理简介: 人工控制的节点: 键盘节点 方向盘节点 自动控制的节点: movebase 导航 autoware 等 底盘节点: 差速底盘 阿克曼底盘 控制节点࿱…...
面试热题(滑动窗口最大值)
给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。 返回 滑动窗口中的最大值 。 输入:nums [1,3,-1,-3,5,3,6,7], k 3 输出:[3,3,5,…...
【代码】表格封装 + 高级查询 + 搜索 +分页器 (极简)
一、标题 查询条件按钮(Header) <!-- Header 标题搜索栏 --> <template><div><div class"header"><div class"h-left"><div class"title"><div class"desc-test">…...
ant.design 组件库中的 Tree 组件实现可搜索的树: React+and+ts
ant.design 组件库中的 Tree 组件实现可搜索的树,在这里我会详细介绍每个方法,以及容易踩坑的点。 效果图: 首先是要导入的文件 // React 自带的属性 import React, { useMemo, useState } from react; // antd 组件库中的,输入…...
Linux系统编程之信号(上)
一、信号概念 信号就是软件中断。每当程序收到一个信号,都需要按指定的方法去处理。以下是UNIX系统的信号表。 其中core表示产生一个复制了该进程内存映像的core文件,它保存了程序现场,可以使用gdb来调试。 二、signal() signal()函数用于改…...
23.Netty源码之内置解码器
highlight: arduino-light Netty内置的解码器 在前两节课我们介绍了 TCP 拆包/粘包的问题,以及如何使用 Netty 实现自定义协议的编解码。可以看到,网络通信的底层实现,Netty 都已经帮我们封装好了,我们只需要扩展 ChannelHandler …...
sigmoid ReLU 等激活函数总结
sigmoid ReLU sigoid和ReLU对比 1.sigmoid有梯度消失问题:当sigmoid的输出非常接近0或者1时,区域的梯度几乎为0,而ReLU在正区间的梯度总为1。如果Sigmoid没有正确初始化,它可能在正区间得到几乎为0的梯度。使模型无法有效训练。 …...
在软件开发中正确使用MySQL日期时间类型的深度解析
在日常软件开发场景中,时间信息的存储是底层且核心的需求。从金融交易的精确记账时间、用户操作的行为日志,到供应链系统的物流节点时间戳,时间数据的准确性直接决定业务逻辑的可靠性。MySQL作为主流关系型数据库,其日期时间类型的…...
Linux链表操作全解析
Linux C语言链表深度解析与实战技巧 一、链表基础概念与内核链表优势1.1 为什么使用链表?1.2 Linux 内核链表与用户态链表的区别 二、内核链表结构与宏解析常用宏/函数 三、内核链表的优点四、用户态链表示例五、双向循环链表在内核中的实现优势5.1 插入效率5.2 安全…...
利用ngx_stream_return_module构建简易 TCP/UDP 响应网关
一、模块概述 ngx_stream_return_module 提供了一个极简的指令: return <value>;在收到客户端连接后,立即将 <value> 写回并关闭连接。<value> 支持内嵌文本和内置变量(如 $time_iso8601、$remote_addr 等)&a…...
Zustand 状态管理库:极简而强大的解决方案
Zustand 是一个轻量级、快速和可扩展的状态管理库,特别适合 React 应用。它以简洁的 API 和高效的性能解决了 Redux 等状态管理方案中的繁琐问题。 核心优势对比 基本使用指南 1. 创建 Store // store.js import create from zustandconst useStore create((set)…...
Mybatis逆向工程,动态创建实体类、条件扩展类、Mapper接口、Mapper.xml映射文件
今天呢,博主的学习进度也是步入了Java Mybatis 框架,目前正在逐步杨帆旗航。 那么接下来就给大家出一期有关 Mybatis 逆向工程的教学,希望能对大家有所帮助,也特别欢迎大家指点不足之处,小生很乐意接受正确的建议&…...
大语言模型如何处理长文本?常用文本分割技术详解
为什么需要文本分割? 引言:为什么需要文本分割?一、基础文本分割方法1. 按段落分割(Paragraph Splitting)2. 按句子分割(Sentence Splitting)二、高级文本分割策略3. 重叠分割(Sliding Window)4. 递归分割(Recursive Splitting)三、生产级工具推荐5. 使用LangChain的…...
React19源码系列之 事件插件系统
事件类别 事件类型 定义 文档 Event Event 接口表示在 EventTarget 上出现的事件。 Event - Web API | MDN UIEvent UIEvent 接口表示简单的用户界面事件。 UIEvent - Web API | MDN KeyboardEvent KeyboardEvent 对象描述了用户与键盘的交互。 KeyboardEvent - Web…...
MySQL 8.0 OCP 英文题库解析(十三)
Oracle 为庆祝 MySQL 30 周年,截止到 2025.07.31 之前。所有人均可以免费考取原价245美元的MySQL OCP 认证。 从今天开始,将英文题库免费公布出来,并进行解析,帮助大家在一个月之内轻松通过OCP认证。 本期公布试题111~120 试题1…...
CRMEB 框架中 PHP 上传扩展开发:涵盖本地上传及阿里云 OSS、腾讯云 COS、七牛云
目前已有本地上传、阿里云OSS上传、腾讯云COS上传、七牛云上传扩展 扩展入口文件 文件目录 crmeb\services\upload\Upload.php namespace crmeb\services\upload;use crmeb\basic\BaseManager; use think\facade\Config;/*** Class Upload* package crmeb\services\upload* …...
根据万维钢·精英日课6的内容,使用AI(2025)可以参考以下方法:
根据万维钢精英日课6的内容,使用AI(2025)可以参考以下方法: 四个洞见 模型已经比人聪明:以ChatGPT o3为代表的AI非常强大,能运用高级理论解释道理、引用最新学术论文,生成对顶尖科学家都有用的…...
