news 2026/9/16 3:56:02

300行代码手写迷你SpringBoot:自动装配与内嵌Tomcat原理实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
300行代码手写迷你SpringBoot:自动装配与内嵌Tomcat原理实战

天天用SpringBoot,IDE里点一下启动按钮,日志刷刷刷就出来了,跟吃饭喝水一样自然。可真要你解释一下“自动装配到底怎么做到的”,大多数人的答案就只剩三句话:读配置文件、加载Bean、启动Tomcat。这三句话没错,但你只背结论、讲不出链路,面试官一句“那你手写个最小版SpringBoot出来看看”就能把你打回原形。

我一直信奉一个笨办法:理解一个框架,与其一头扎进那几十万行源码里跟它死磕,不如先手写一个能跑的迷你版。这次我就用300行代码,手写一个简化版的SpringBoot核心原理,不追求功能完整,只求把“启动流程、包扫描、依赖注入、内嵌Tomcat、自动配置”这五件事的底层链路完整跑通。这篇东西适合两类人:一种是准备SpringBoot面试、想真正把原理讲清楚的人;另一种是用SpringBoot写了不少业务,但总觉得“框架黑盒”心里没底的人。跟着我写完,你自己都能把启动过程给别人讲明白。

1. 先想清楚:SpringBoot到底替你做了什么

很多人对SpringBoot的印象是“约定优于配置”,这个说法对,但太虚。在动手之前,先把SpringBoot替我们干的几件大事拆出来,你才知道那300行代码该往哪使劲。

1.1 三个最核心的能力:内嵌容器、自动装配、配置约定

第一件大事是内嵌Servlet容器。在SpringBoot没有普及的年代,一个Web项目要经历打war包、丢进Tomcat的webapps目录、启动外部Tomcat、再等部署完成这一整套流程。SpringBoot直接把Tomcat变成Maven依赖,用代码创建和启动,你一条java -jar命令就能把整个应用拉起来。这个转变的内核,就是把原来外部容器的启动动作,搬进了你自己的main方法里。

第二件大事是自动装配。往pom.xml里加一个spring-boot-starter-data-redis,Redis的连接工厂、模板工具类就被自动准备好了;加一个spring-boot-starter-web,内嵌Tomcat和DispatcherServlet就自动就位了。这个过程靠的不是魔法,而是SpringFactoriesLoader机制配合条件注解。这也就是面试题里最常问的@EnableAutoConfiguration原理。

第三件大事是配置约束与约定。比如@SpringBootApplication默认扫描它所在包及子包,比如自动配置类通过@ConditionalOnClass@ConditionalOnMissingBean来判断“当前环境到底该不该装配某个Bean”,再比如通过@ConfigurationProperties把配置文件绑定成强类型对象。这三件事共同构成了SpringBoot的开发体验。

另外还有个容易被忽略的小细节——启动Banner。你每次启动时看到的那个ASCII艺术字,其实是SpringApplication在run最开始阶段渲染出来的,它本身不是核心功能,但恰恰说明SpringBoot把“启动过程”做成了一个可扩展的流水线。

1.2 为什么要手写,以及手写能换来什么

有人可能会说:“SpringBoot源码我读了呀,类图也画了,咋还是感觉不稳?”原因很简单:读源码是被动接收信息,手写是主动构造链路。你看10遍BeanPostProcessor的处理时序,不如自己写一遍依赖注入失败再修好的过程来得深刻。

300行这个规模很巧妙,它刚好能覆盖SpringBoot最主干的那条链路,又不会多到让你失去耐心。你可以把它理解成拆发动机:不用拆到每一个活塞环,但核心的“进气-压缩-做功-排气”四冲程你必须亲眼看到。写完之后,你再看SpringApplication.run那一大段源码,会发现很多类名和方法名都很眼熟,因为它们就是这套迷你逻辑的完整工业版。

2. 整体设计:300行代码怎么分配

手写之前先规划,别上来就写。SpringBoot再复杂,它的起步动作也就是“创建容器、扫描Bean、启动Web服务器”这三板斧。所以我的迷你版也按照这个顺序来设计代码结构。

2.1 技术底座:只用Tomcat embed,不碰Spring

这个项目我没有引入任何Spring依赖,唯一的第三方依赖是tomcat-embed-core。这个包把真实的Tomcat核心打包成了一个普通Jar,我们直接new Tomcat就行。

选择Tomcat 9.x是因为它还是javax.servlet命名空间,Tomcat 10以后切到了jakarta.servlet,如果你的项目是JDK17或SpringBoot 3.x,要留意这个包名迁移问题,很多老项目从JDK8升到JDK21时踩的坑都卡在这。POM依赖长这样:

<dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-core</artifactId> <version>9.0.83</version> </dependency>

其余全部基于JDK自带的反射、注解和类加载器。之所以这么穷酸,是为了把框架的职责暴露出来:扫描用File和ClassLoader,注入用反射,条件判断用Class.forName,够用就行。

2.2 类清单与行数规划

先列个清单,心里有数再动工。整体分为注解、容器、Web、自动配置、示例代码五类:

模块类名职责预估行数
启动入口MiniApplicationmain方法编排启动流程20
启动配置AppConfig配置扫描包路径10
注解MiniApplication启动注解,携带包扫描配置10
注解MiniComponent、MiniAutowired组件注册与依赖注入10
注解MiniGetMappingURL映射10
注解MiniAutoConfiguration、MiniConditionalOnClass自动配置与条件判断10
容器ApplicationContext扫描、注册、注入、保存Bean80
容器ClassScanner按包路径扫描class文件30
WebTomcatServer创建并启动内嵌Tomcat30
WebDispatcherServlet请求分发到Controller方法60
自动配置AutoConfigurationLoader加载自动配置类35
示例HelloController、HelloService、JsonAutoConfiguration验证整体链路45

加起来正好300行上下。注意,这里说的是“可以不用Spring全家桶实现主干逻辑”,示例Controller之类属于验证代码,你完全可以再减少。逻辑上,这个迷你框架就是三个核心类在干活:ApplicationContext负责Beans,TomcatServer负责Web容器,DispatcherServlet负责请求分发。

3. 先把原理打通:自动装配这件事

写代码之前,我建议先把自动装配的底层逻辑捋一遍。因为这是SpringBoot面试出题率最高的点,也是手写时最容易出“顺序”问题的环节。

3.1 从war包到内嵌服务器的思路转变

传统SSM项目里,Tomcat是先于你的应用存在的。你的工程打出一个war包,丢到Tomcat的webapps目录下,Tomcat启动时通过Servlet规范去解析war包的WEB-INF/web.xml,然后加载Spring的ContextLoaderListener,最后才初始化DispatcherServlet。整套流程里,Spring是“住在”Tomcat里的房客。

SpringBoot把这个顺序倒过来了:你的main方法先创建一个应用上下文,按需加载Bean,然后这段逻辑里主动new一个Tomcat实例,把自己的DispatcherServlet塞进去,最后tomcat.start()。这个思路上的转变如果没有建立起来,你后面看WebServerFactoryCustomizer和ServletContextInitializer时只会一脸懵。在手写时,这个顺序是被强制体现的:先有Context,再有Server,Server里再挂DispatcherServlet。

3.2 spring.factories与SPI机制:发现自动配置类的钥匙

Java世界有一套经典的插件化机制,叫SPI。简单说,核心框架定义好一个接口,扩展方把实现类的全限定名写进META-INF/services/接口全限定名这个文件里,运行的时候框架通过ClassLoader扫描到这些名字,再用反射加载实现类。这就像你家墙上预留好了插座孔,不同品牌的电器只要按照同一个标准做插头,插上就能供电。

SpringFactoriesLoader就是Spring框架对SPI机制的自定义实现。它会在META-INF/spring.factories里找key为EnableAutoConfiguration的值,这些值就是一个个自动配置类的全限定名。SpringBoot启动时,AutoConfigurationImportSelector通过它拿到候选配置类列表,再逐个交给条件注解去筛选。手写的时候没必要复刻整个文件系统扫描,用一个String数组硬编码模拟候选列表,反而更容易看清本质。

3.3 条件装配是自动配置的灵魂

自动配置类不是无脑全部装配,它必须根据当前classpath和环境来决定“干不干活”。最典型的是Redis的自动配置类,它上面有@ConditionalOnClass(RedisOperations.class),意思是你必须引入了spring-data-redis依赖,RedisOperations这个类才会出现在classpath里,自动配置才会生效。这就像很多家庭的智能插座检测到“有大功率电器接入”才通电,SpringBoot的Conditional系列注解就是那个检测器。

面试时讲自动装配,你一定要讲出这一层:自动配置类是“候选”,条件注解是“开关”,两者配合才叫完整的自动装配。光说“读取spring.factories”只能拿一半分。

4. 实操实现:一个简化版SpringBoot诞生记

理论铺垫完,开始敲代码。我会按启动入口、容器、Web服务器、请求分发、自动配置这五个环节逐个拆解,每一步都会标注“为什么这么做”,而不是只给代码。

4.1 入口:MiniApplication与注解定义

真实SpringBoot的入口是一个带@SpringBootApplication的类,里面写上SpringApplication.run(App.class, args)。迷你版照葫芦画瓢:

public class MiniApplication { public static void main(String[] args) throws Exception { ApplicationContext context = new ApplicationContext(AppConfig.class); TomcatServer server = new TomcatServer(context); server.start(8080); } }

看到没有,整个启动流程只有三行:创建容器、创建Web服务器、启动服务器。真实SpringBoot的SpringApplication.run内部做的远不止这些,但它最核心的骨架就是这三步。

AppConfig负责告诉框架扫哪个包:

@MiniApplication(scanBasePackages = "com.example") public class AppConfig { }

再来看注解定义。我用了四个自定义注解,分别对应真实SpringBoot里的@SpringBootApplication@Component@Autowired@GetMapping@ConditionalOnClass的简化版:

@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface MiniApplication { String scanBasePackages() default ""; } @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface MiniComponent { } @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface MiniAutowired { } @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface MiniGetMapping { String value(); } @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface MiniAutoConfiguration { } @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface MiniConditionalOnClass { String value(); }

写注解的时候有两个细节:@Retention(RetentionPolicy.RUNTIME)必须加,否则运行期反射拿不到;字段注解的Target要写FIELD,方法注解的Target要写METHOD,别混。真实SpringBoot里@SpringBootApplication是一个组合注解,由@SpringBootConfiguration@EnableAutoConfiguration@ComponentScan三个注解组成,它的包扫描默认值也是从该注解所在类的包开始的。

4.2 IOC容器:扫描、注册、注入一个都不能少

这是迷你框架里最重的一块。ApplicationContext的构造函数做了三件事:扫描指定包下的class,过滤出带@MiniComponent的类并创建实例,最后统一执行字段注入。

public class ApplicationContext { private final Map<String, Object> beans = new ConcurrentHashMap<>(); public ApplicationContext(Class<?> configClass) throws Exception { // 1. 获取扫描包路径 MiniApplication app = configClass.getAnnotation(MiniApplication.class); String basePackage = app.scanBasePackages(); if (basePackage.isEmpty()) { basePackage = configClass.getPackage().getName(); } // 2. 先注册普通组件 Set<Class<?>> classes = ClassScanner.scan(basePackage); for (Class<?> clazz : classes) { if (clazz.isAnnotationPresent(MiniComponent.class)) { String beanName = firstLower(clazz.getSimpleName()); beans.put(beanName, clazz.getDeclaredConstructor().newInstance()); } } // 3. 加载自动配置类,必须先于依赖注入 AutoConfigurationLoader.load(this); // 4. 统一执行字段注入 for (Object bean : beans.values()) { inject(bean, new HashSet<>()); } } private void inject(Object bean, Set<String> visiting) throws IllegalAccessException { Field[] fields = bean.getClass().getDeclaredFields(); for (Field field : fields) { if (field.isAnnotationPresent(MiniAutowired.class)) { String name = firstLower(field.getType().getSimpleName()); Object dependency = beans.get(name); if (dependency == null) { throw new IllegalStateException("找不到依赖: " + name); } field.setAccessible(true); field.set(bean, dependency); } } } public Object getBean(String name) { return beans.get(name); } public Collection<Object> getBeans() { return beans.values(); } public void register(Object bean) { beans.put(firstLower(bean.getClass().getSimpleName()), bean); } private String firstLower(String name) { return name.substring(0, 1).toLowerCase() + name.substring(1); } }

这里的beanName策略取自“类名首字母小写”,真实Spring的BeanName生成器会考虑一个类多个实例、特殊命名等情况,但教学场景这样够用。我特意在构造函数里把自动配置加载放在依赖注入之前,因为后续的Service可能要注入自动配置类产生的Bean,如果先注入再加载,那些字段就全是null——这个问题后面还会展开讲。

ClassScanner的实现非常简单,它用ClassLoader找到包对应的目录,再遍历目录下的所有class文件,加载成Class对象:

public class ClassScanner { public static Set<Class<?>> scan(String packageName) throws Exception { Set<Class<?>> classes = new HashSet<>(); String path = packageName.replace('.', '/'); ClassLoader loader = Thread.currentThread().getContextClassLoader(); Enumeration<URL> resources = loader.getResources(path); while (resources.hasMoreElements()) { URL resource = resources.nextElement(); File dir = new File(resource.toURI()); for (File file : dir.listFiles()) { if (file.getName().endsWith(".class")) { String className = packageName + "." + file.getName().replace(".class", ""); classes.add(Class.forName(className)); } } } return classes; } }

注意:这个扫描器只能扫目录形式的class文件,打成Jar之后new File(resource.toURI())会炸。这是我刻意保留的“缺陷”,后面讲局限时会用。

4.3 内嵌Tomcat:三步挂起一个Web应用

Web服务器这一部分,只要理解了Tomcat提供了一套可编程API就很简单。创建Tomcat实例、设置端口、添加Context、往Context里注册Servlet和URL映射,一共三步:

public class TomcatServer { private final ApplicationContext context; public TomcatServer(ApplicationContext context) { this.context = context; } public void start(int port) throws Exception { Tomcat tomcat = new Tomcat(); tomcat.setPort(port); tomcat.getConnector(); Context ctx = tomcat.addContext("", new File(".").getAbsolutePath()); Tomcat.addServlet(ctx, "dispatcher", new DispatcherServlet(context)); ctx.addServletMappingDecoded("/*", "dispatcher"); tomcat.start(); tomcat.getServer().await(); } }

tomcat.getConnector()这行很关键,它是把内置连接器创建出来并绑定到端口上的动作。addContext("", ...)表示根路径上下文,第二个参数是docBase,指向当前项目目录。然后通过Tomcat.addServlet注册一个名为dispatcher的Servlet,再调用addServletMappingDecoded("/*", "dispatcher")把根路径下所有请求都交给这个Servlet处理。最后tomcat.getServer().await()会让主线程阻塞,让Tomcat持续运行。

为什么DispatcherServlet要接管/*?因为SpringMVC的核心思想就是前端控制器模式:所有请求先进来,由DispatcherServlet根据路径分发给具体Controller。我们的迷你版也是这样设计的。

4.4 DispatcherServlet:手写前端控制器

DispatcherServlet继承HttpServlet,覆盖init()service()两个方法。init阶段扫描容器里每个Bean的方法,把带@MiniGetMapping的方法和URL记录下来;service阶段就拿来一个请求URI,从映射表里找到对应方法,用反射执行并返回结果。

public class DispatcherServlet extends HttpServlet { private final ApplicationContext context; private final Map<String, Method> methodMapping = new HashMap<>(); private final Map<String, Object> beanMapping = new HashMap<>(); public DispatcherServlet(ApplicationContext context) { this.context = context; } @Override public void init() { for (Object bean : context.getBeans()) { Method[] methods = bean.getClass().getDeclaredMethods(); for (Method method : methods) { if (method.isAnnotationPresent(MiniGetMapping.class)) { MiniGetMapping mapping = method.getAnnotation(MiniGetMapping.class); methodMapping.put(mapping.value(), method); beanMapping.put(mapping.value(), bean); } } } } @Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException { String uri = req.getRequestURI(); Method method = methodMapping.get(uri); Object bean = beanMapping.get(uri); resp.setContentType("text/html; charset=utf-8"); if (method == null || bean == null) { resp.setStatus(404); resp.getWriter().write("404 Not Found"); return; } try { Object result = method.invoke(bean); resp.getWriter().write(String.valueOf(result)); } catch (Exception e) { resp.setStatus(500); resp.getWriter().write("500 Internal Error: " + e.getCause().getMessage()); } } }

这个类就是SpringMVC DispatcherServlet的极简影子。真实版HandlerMapping会处理Ant通配符路径、方法参数解析、返回值适配、@ResponseBody序列化等一大堆逻辑,但核心分发思路一模一样。写到这里你会发现,原来Controller方法的映射本质上就是一个“URL -> Method”的Map查询过程,没那么多玄机。

配套示例Controller和Service:

@MiniComponent public class HelloController { @MiniAutowired private HelloService helloService; @MiniGetMapping("/hello") public String hello() { return helloService.sayHello(); } } @MiniComponent public class HelloService { @MiniAutowired private JsonAutoConfiguration jsonAutoConfiguration; public String sayHello() { return "mini springboot say: " + jsonAutoConfiguration.getOutputType(); } }

4.5 自动配置加载器:模拟Starter的感觉

接下来是重头戏,AutoConfigurationLoader。它做的事情非常接近SpringFactoriesLoader:读取候选自动配置类列表,判断条件注解是否满足,满足就把类的实例也注册进容器。

public class AutoConfigurationLoader { // 模拟 META-INF/spring.factories 中的 EnableAutoConfiguration 配置项 private static final String[] AUTO_CONFIGURATIONS = { "com.example.autoconfig.JsonAutoConfiguration" }; public static void load(ApplicationContext context) throws Exception { for (String className : AUTO_CONFIGURATIONS) { Class<?> clazz = Class.forName(className); if (!clazz.isAnnotationPresent(MiniAutoConfiguration.class)) { continue; } MiniConditionalOnClass condition = clazz.getAnnotation(MiniConditionalOnClass.class); if (condition != null && !isPresent(condition.value())) { System.out.println("[AutoConfig] 跳过 " + className + ",缺少类 " + condition.value()); continue; } context.register(clazz.getDeclaredConstructor().newInstance()); System.out.println("[AutoConfig] 加载 " + className); } } private static boolean isPresent(String className) { try { Class.forName(className); return true; } catch (ClassNotFoundException e) { return false; } } }

再配一个自动配置类:

@MiniAutoConfiguration @MiniConditionalOnClass("com.fasterxml.jackson.databind.ObjectMapper") public class JsonAutoConfiguration { public String getOutputType() { return "json"; } }

这里@MiniConditionalOnClass("com.fasterxml.jackson.databind.ObjectMapper")模拟的是“只有classpath里存在Jackson才装配”。如果项目没有引入Jackson依赖,这个自动配置类就会被跳过。跑起来以后的启动日志长这样:

[AutoConfig] 加载 com.example.autoconfig.JsonAutoConfiguration

打开浏览器访问http://localhost:8080/hello,你会看到:

mini springboot say: json

到这里,一个能跑的迷你SpringBoot闭环就完成了。

5. 调试与踩坑实录

手写这套东西的过程,踩坑是不可避免的。有些坑恰恰是理解SpringBoot生命周期的钥匙,我挑几个最有价值的记录下来。

5.1 自动配置Bean是null:依赖注入的顺序不能乱

第一个巨坑是加载顺序。最开始我的写法是ApplicationContext构造结束后,再由main方法里调用AutoConfigurationLoader.load(context)。结构看起来很美,但一运行就报“找不到依赖: jsonAutoConfiguration”。

原因很简单:ApplicationContext的构造函数在扫描完普通组件后立刻执行了字段注入,而自动配置Bean还没被注册进容器,HelloService里的@MiniAutowired字段自然找不到目标。后来我把自动配置加载挪到了容器构造函数的第三步,让“扫描普通Bean -> 加载自动配置Bean -> 统一注入”的顺序固定下来,问题就解决了。

这个坑特别值得讲,因为它对应了SpringBoot真实设计里Bean定义注册和Bean实例化两个阶段。Spring容器里不是扫描完立刻实例化所有Bean,而是先收集BeanDefinition,再在refresh()finishBeanFactoryInitialization阶段才实例化单例。自动配置类之所以能在真实容器里正常工作,正是因为它的Bean定义在实例化之前就被导入并注册了。

5.2 扫描不到类:文件扫描方式的天然局限

另一类常见问题是扫描路径写错或者打包成Jar后ClassScanner失效。我们的ClassScanner通过loader.getResources(path)拿到URL后转成File再遍历,这在IDEA里运行没问题,因为target/classes就是目录结构。但打成Jar包后class文件是压缩在Jar内部的,File方式根本访问不到。

真实SpringBoot用ASM配合ClassPathScanningCandidateComponentProvider,既可以扫描文件目录,也可以扫描Jar内部资源。这也是为什么很多框架要引入字节码操作库的原因——不是闲得慌,是为了能在各种部署形态下正确地发现组件。我这里故意保留了这个小缺陷,就是让你体会一下,一个看起来简单的“扫描包”,在真实场景里需要考虑多少东西。

5.3 端口占用、start后立刻退出:Tomcat开发时的小麻烦

调试过程中最容易遇见的两个Tomcat小麻烦,一个是8080端口被占用,一个是Tomcat启动后主线程退出直接结束程序。端口问题直接用tomcat.setPort(8081)换端口就能规避,或者查占用进程kill掉。

第二个问题是因为忘了加tomcat.getServer().await()。Tomcat启动后,如果主线程马上执行完main方法,JVM会退出,容器也就停了。await()让主线程在这里阻塞等待,Tomcat才会持续对外服务。真实的SpringBoot当然也调用了await机制,只是封装在SpringApplication内部而已。

5.4 与真实SpringBoot的差异对比

每个坑背后其实都是一条知识点。我把这套迷你版和真实SpringBoot的差异整理成一个对照表:

能力维度真实SpringBoot300行迷你版
类扫描ASM扫描,支持目录与Jar包反射+File扫描,只支持目录结构
依赖注入完整生命周期,支持循环依赖、代理反射字段注入,不支持循环依赖
自动配置spring.factories + 几十种Conditional条件硬编码数组 + 单个ConditionalOnClass
Web容器启动WebServerFactory + 自动配置Tomcat embed API手动硬编码
配置绑定Environment + ConfigurationProperties没有配置解析
AOP/事务内置或通过starter提供没有,需要自行扩展

这张表同时也是一张线索图,照着它能知道真实SpringBoot在哪些方向上做得更深。你读源码的时候会发现,它的每个模块都对应着这些差异的某个解法。

6. 这个迷你项目的延伸价值

写到这里,你已经拥有一个能跑通的迷你框架了。但它的真正价值不只是验证一条链路,而是能帮你办成两件具体的实事。

6.1 面试被问自动装配时,你可以这样答

有这套东西打底,面对“SpringBoot自动装配原理”这个问题,你的回答可以自成体系:

第一句,先指出入口:@SpringBootApplication是组合注解,其中@EnableAutoConfiguration是自动装配的总开关;第二句,讲机制:它内部通过AutoConfigurationImportSelector调用SpringFactoriesLoader,读取META-INF目录下spring.factories文件里所有EnableAutoConfiguration的配置项,得到候选自动配置类列表;第三句,讲过滤:每个候选类上都有一组@Conditional条件注解,比如@ConditionalOnClass要求classpath存在指定类才生效;第四句,讲结果:满足条件的配置类被导入容器,它内部用@Bean定义各种组件,最终完成自动装配。

你要是还能补一句“条件不满足的配置类会在启动日志里被标记为ConditionEvaluationReport”,面试官基本就知道你是真看过源码的人。这套逻辑你现在已经亲手实现过一遍,讲出来比背面试题自然太多。

6.2 下一步还能怎么扩展

如果你想把这套迷你框架继续玩下去,我建议按下面的顺序加功能:先加一个BeanPostProcessor接口,模拟真实Spring里允许每个Bean在初始化前后被修改的特性;再加一个@MiniAspect注解,配合JDK动态代理做最简单的切面;最后可以试着解析一个application.properties文件,把server.port这种配置真正读出来绑到TomcatServer上。每加一个功能,都会遇到一个新的“为什么不这样做就崩了”的问题,那才是框架设计最值钱的部分。

顺便一提,现在SpringBoot还在发展,比如MCP这样的新能力就是通过标准协议让应用能接入AI模型能力,但它底层的容器、装配、配置骨架并没有变。你把基础链路打通了,新特性对你而言只是在这套地基上多搭了一层楼。

300行代码写完的那个晚上,我又回去翻了翻SpringApplication.run的源码,发现我们的主流程和它一模一样:创建上下文、加载候选配置、准备Web服务器、启动。那一刻确实挺有成就感的。

我个人最大的体会是,框架这东西,越是天天用,越要用“过一遍手写版”来打破黑盒恐惧。你不用学我写300行,哪怕只写一个100行的最小IOC容器,自己对“注解到底是怎么起作用的”就会有完全不一样的感知。下次启动SpringBoot看到那个Banner刷出来的时候,你可能心里会默默想:哦,这里不过就是几行我写过的代码加上一个漂亮的皮肤罢了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 3:55:59

Windows 11安装eNSP启动失败40?全套排坑与VMware隔离方案

最近不少人在Windows 11上装eNSP&#xff0c;装完一启动AR路由器就卡在井号&#xff0c;或者直接报错“启动设备AR1失败40”。网上说法很多&#xff0c;什么WinPcap没装好、VirtualBox版本不对、要关Hyper-V&#xff0c;看得人头晕。我前前后后折腾了三天&#xff0c;把Windows…

作者头像 李华
网站建设 2026/9/16 3:55:44

基于MATLAB的参数已知GLRT信号检测仿真:从匹配滤波到ROC曲线

简介&#xff1a;MATLAB参数已知条件下的GLRT信号检测仿真&#xff0c;面向通信、雷达等需要掌握广义似然比检验原理的研究生、工程师及高校相关专业学生。围绕随机信号检测中零假设与备择假设的判定问题&#xff0c;资源以完整工程代码呈现似然函数建模、对数似然比计算、参数…

作者头像 李华
网站建设 2026/9/16 3:54:02

QLoRA实战:4-bit量化+LoRA微调,7B模型在8GB显存上高效训练

简介&#xff1a;这是一套面向AI算法工程师与大模型研究者的量化微调实践工具包&#xff0c;聚焦于低资源环境下大规模语言模型&#xff08;LLM&#xff09;的高效适配与部署。QLoRA工具通过量化感知微调技术&#xff0c;在显著降低显存占用的同时保持模型性能&#xff0c;适用…

作者头像 李华
网站建设 2026/9/16 3:54:00

vivago R1本地部署实操:60秒AI视频生成全链路解析

1. 这不是“无限时长”&#xff0c;而是60秒内把《华强买瓜》演活的硬核实操最近刷到一条视频&#xff0c;标题写着“‘无限时长’AI视频生成来了”&#xff0c;点进去一看——60秒&#xff0c;画面稳定、台词精准、人物微表情有戏&#xff0c;连华强推墨镜时手指关节的轻微弯曲…

作者头像 李华
网站建设 2026/9/16 3:53:50

带AI的青少年心理健康管理系统:SpringBoot+Vue毕设完整实现指南

做毕设选了这个题目的同学&#xff0c;我想先给你吃一颗定心丸&#xff1a;“带AI的青少年心理健康管理系统”是一个性价比极高的选题。它表面上是SpringBootVue的经典前后端分离项目&#xff0c;但实际上通过一个“AI模块”把整个项目的技术深度和创新性都提上来了——既有管理…

作者头像 李华
网站建设 2026/9/16 3:53:12

磁盘I/O %util飙高?从iostat到根因定位的实战排查指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华