
NanoBoot一个主打轻量的Java框架目标是在不动摇依赖注入和HTTP路由这些核心能力的前提下把启动时间压到1秒以内、运行时内存控制在几十MB级别。如果你也在做中小规模的接口服务、嵌入式工具或者单纯想搞懂Java注解和反射到底是怎么落地成框架的这篇文章值得看完。我花了一周时间把NanoBoot从零写完过程中踩了不少坑也把SpringBoot那套启动逻辑简化成了只有几百行的核心引擎。下面是我对这个框架的完整设计复盘和可复现的实现路径。很多人第一次听NanoBoot会问已经有了SpringBoot为什么还要自己造一个轮子这个问题的答案其实也是我写NanoBoot的初衷。SpringBoot固然强大但它在微型服务、边缘设备、教学演示这类场景里确实显得“重”了一个空的web项目打底就要几十个依赖、上百万字节的jar包启动内存轻松超过200MB冷启动时间经常1秒开外。而NanoBoot压缩到现在这个形态只提供四个核心能力组件扫描、依赖注入、请求路由、属性配置。没有花哨的自动配置、没有复杂的条件装配但足够撑起一个日活百万以下的中小型接口服务。1. 项目设计与定位解析1.1 为什么需要一个“极轻”的Java框架先说说我踩过的实际情况。之前做一个物联网网关的采集服务部署在只有512MB内存的ARM盒子上SpringBoot起来之后堆内存占掉300MB剩下200MB要同时跑采集线程和协议解析器经常被系统淘汰。当时试着去掉web功能只保留Spring核心但麻烦事接踵而至——自己组装Spring容器比直接写一个轻量框架还费劲而且Spring为了兼容各种环境底层对象链拉得很长光是启动时扫描BeanDefinition就做了大量我们根本不需要的工作。NanoBoot在设计时就定了几条硬杠杠第一外部依赖尽量为零用JDK自带的HTTP Server而不是Tomcat或Netty核心代码不引入第三方库第二容器只支持单例Bean和构造器/字段注入不搞原型作用域、不搞BeanPostProcessor链第三启动过程中不做AOT字节码增强纯注解扫描加反射调用。这一套组合拳下来NanoBoot空项目的启动时间基本稳定在80毫秒左右堆内存占用不到30MB连一个完整jar包都只有15KB左右。对比下来你会发现我并不是说SpringBoot不好而是说“场景要匹配”。NanoBoot能解决的问题是一类特定的问题启动要快、占用要低、依赖要少、逻辑要透明。所以它更适合的场景是物联网边缘采集、内网小工具、云函数本地调试、教学演示以及想搞懂“框架底层到底怎么运行”的源码学习者。1.2 核心设计目标与技术选型NanoBoot的选型决策表如下每一行都对应着一个真实权衡。设计维度NanoBoot的选择放弃的选项原因Web服务器JDK内置HttpServerTomcat / Netty零依赖启动快应付常规请求足够配置格式Properties文件YAML解析YAML需要引入snakeyaml违背轻量原则依赖注入字段注入反射赋值构造器注入/三级缓存代码最少循环依赖直接报错更安全路由实现注解加反射注册字节码增强实现简单可读性强适合中小接口AOP能力提供处理器拦截接口动态代理切面保持核心稳定AOP由扩展模块实现启动入口注解扫描加SPI目录SpringFactories机制只扫用户配置的包不扫描全classpath这里面最关键的取舍是“不用Tomcat”。很多Java开发者习惯了Tomcat这种“标配”但JDK内置的com.sun.net.httpserver.HttpServer在JDK 8开始就存在支持HTTP/1.1、支持动态Context注册对短连接、低并发场景完全够用。实测同一个HelloWorld接口Tomcat冷启动需要200ms建立连接器而JDK HttpServer在JVM启动后几乎立即可用而且线程模型是自己可控的不会起一堆后台线程。另一个重要取舍是“不做三级缓存”。Spring解决循环依赖靠三层缓存加代理提前暴露这套机制非常精巧但代价是容器结构复杂。NanoBoot面对的Bean规模通常只有几十个如果出现循环依赖最合理的处理就是启动时直接报错告诉开发者谁引用了谁让你主动去重构依赖关系。这种“有所不为”的思路反而让容器代码量控制在一个很小的区间。1.3 整体架构与模块划分NanoBoot的源码分成四个模块编译器期零依赖运行时只依赖JDKnanoboot-core核心容器负责扫描、实例化、依赖注入、ApplicationContext管理。nanoboot-webHTTP模块解析RequestMapping注解注册处理器完成请求分发。nanoboot-config配置模块读取application.properties并把配置值注入到NanoValue字段。nanoboot-extension可选扩展包括定时任务、简单拦截器、JSON输出适配。架构上把core和web分开是为了让不需要Web能力的任务型程序也能复用容器。比如我只写一个定时采集任务直接在main方法里调NanoBootApplication.runTask()它只会启动容器和执行NanoScheduled方法根本不会创建HTTP端口这样内存还能再省8~10MB。整个应用层的启动顺序是这样的加载主类的NanoBootApplication注解拿到扫描包和端口参数创建NanoContext并初始化容器然后向容器注册内置Bean如配置解析器、路由分发器最后如果有Web能力就启动HTTP Server。所有模块之间通过简单接口通讯不搞复杂的监听器链。2. 核心机制与关键设计拆解2.1 注解驱动的IoC容器与扫描器NanoBoot的IoC容器是一个用MapClass?, Object实现的单例池所有被NanoComponent标记的类都会在启动时被扫描并实例化两次先创建空实例放入“预创建池”再统一执行依赖注入。这样设计的好处是即使两个Bean存在非循环的启动依赖关系也能在第二次遍历时全部填充完成。扫描器的实现核心是用ClassPathScanning逻辑遍历指定包下所有.class文件读取文件名并截取类名然后通过Class.forName()加载。这里有个容易忽略的细节在实际项目中包名对应文件路径所以要把com.example.app转换成com/example/app用ClassLoader.getResources(path)拿到URL后打开目录流逐个文件遍历。实测下来扫描一个包含200个类的包耗时不到15ms完全够用。拿到Class数组后遍历检查类上是否有NanoComponent注解。有则创建实例没有则跳过。这一步里必须处理匿名内部类、局部类、接口、抽象类和枚举——这些都是无法实例化的类型一旦混进去会导致InstantiationException。我在第一版就吃过这个亏扫描到枚举类直接崩后来加了一个过滤条件判断Modifier.isAbstract或者isInterface就跳过。注入阶段遍历的是类声明的字段查找NanoAutowired注解如果字段类型能直接从单例池取到实例就直接写入。NanoBoot的注入规则很简单先用字段类型从小池子找找不到再从池子里找可赋值类型比如接口找实现类但要求实现类只有一个。如果找到多个实现类直接报错不搞按名称回退的模糊逻辑。这种“宁可报错也不猜”的策略虽然粗暴但调试成本最低。2.2 路由注册与请求分发机制Web模块的路由表保存在一个MapString, HandlerMeta中key是请求方法加路径比如GET /api/uservalue封装了处理该请求的Controller实例、Method对象、方法参数名列表以及参数类型。注册时机发生在容器完成注入后框架会遍历单例池里所有实例检查其Class上是否有NanoController注解再逐个方法检查NanoMapping。这里设计的重点是“参数绑定规则”。NanoBoot处理HTTP参数的方式很朴素优先从URL查询参数?a1b2取取不到就尝试从POST表单里取再取不到就尝试从JSON body里取——但这个尝试顺序在代码里是显式可控的。方法参数上用NanoParam(name)指定参数名如果没有注解就用JDK反射拿真实参数名这要求编译时开启-parameters参数否则参数名会被编译器抹掉变成arg0、arg1。请求分发器实现的是HttpHandler接口每次收到请求都做这样几件事解析出HTTP方法、URI路径用这两个值去路由表里匹配HandlerMeta匹配成功后从HttpExchange的请求头、查询串、请求体里取参数并绑定到反射调用通过反射调用方法得到返回值把返回值序列化成JSON或纯文本写回响应流。整套分发过程看起来简单但有个性能陷阱值得提醒每个请求如果都做完整反射链路在高并发下会放大GC压力。我的做法是在路由注册阶段就把Method.setAccessible(true)调用一次并缓存后续调用直接用同一个Method对象避免重复做访问权限检查。同时把参数绑定器ParameterBinder作为策略接口重写了好几版内置了String、int、long、boolean等基础类型的转换器这些转换器实例也是注册到容器里的单例不随请求创建。2.3 属性配置加载与占位符替换配置模块负责加载类路径下的application.properties并把配置值注入到标了NanoValue(${server.port})的字段里。我把这个设计得非常克制的点在于NanoBoot的配置读取“不做多环境Profile、不做配置中心、不做动态刷新”只做最基础的文件加载和占位符替换。加载流程是固定的先读classpath根目录下的application.properties如果存在就加载成Properties对象存储到配置容器中然后遍历所有单例Bean检查每个字段的NanoValue用驼峰命名规则从配置容器取value再经过类型转换后写入字段。如果配置值缺失可以指定默认值写法是NanoValue(${server.port:8080})。这里面有一道容易出错的工序占位符解析是支持嵌套的比如NanoValue(${server.base-url:http://${server.host}:${server.port}})。我用一个递归解析函数处理这种嵌套但设置了最大递归深度10层防止有人写了个循环引用占位符导致栈溢出。解析完成后如果发现字符串里还有未替换的${就说明配置缺失启动直接报错并指出字段全名绝不在运行期返回一个“看起来对了但实际上错了”的字符串。2.4 扩展机制与SPI目录设计NanoBoot的扩展点在源码里贯彻得比较干净目录META-INF/services/nanoboot-extensions.properties里用一行一个类名声明扩展模块框架启动时用ServiceLoader加载接口实现。这个设计思路来自SPI但比Java原生的ServiceLoader.load()更好调试因为加载不到扩展时会打印具体短类名而不是一句笼统的ProviderNotFound。目前官方扩展模块有三个定时任务模块读取NanoScheduled(cron *\/5 * * * * ?)用一个后台线程池按cron表达式触发方法调用拦截器模块在分发器外面包了一层Filter链用户可以实现NanoInterceptor接口做统一鉴权、日志打印、响应头添加JSON模块内置了一个极简序列化器支持集合、Map、数组和普通POJO不依赖Jackson但同样因为不使用Jackson所以不支持泛型丢失后的复杂反序列化。做扩展机制时我最深刻的体会是框架作者最容易犯的错是“把所有需求都塞进核心”。一开始NanoBoot的core模块想集成JSON序列化后来发现JSON解析和序列化本身就是个大坑而且每个项目的需求都不同有人要忽略空字段有人要格式化缩进。于是我把序列化抽成接口默认实现是一套几十行代码的简易功能如果用户有Jackson依赖扩展模块里再提供适配器。这样核心永远保持“最小必要”扩展看场景自由选择。3. 从零实现NanoBoot核心代码3.1 工程结构与最低依赖配置为了让你能直接照着复刻我把工程搭建过程写详细些。整个项目用Maven管理JDK 11parent不继承Spring任何版本pom文件只需要dependencies里留空——真正的依赖只有JDK自带类库。properties maven.compiler.source11/maven.compiler.source maven.compiler.target11/maven.compiler.target project.build.sourceEncodingUTF-8/project.build.sourceEncoding /properties注意编译源码时要加parameterstrue/parameters目的就是前面提到的保留方法参数名。如果这一步忘了配反射拿到的参数名会变成arg0、arg1NanoParam没写时就会绑错参数。代码结构保持经典的src/main/java包路径建议是io.nanoboot.core、io.nanoboot.web、io.nanoboot.config、application避免把主类放在默认包下因为默认包类无法被Class.forName基于文件路径正常扫描。3.2 启动注解与启动器实现NanoBoot的启动逻辑全部收敛在一个NanoBootApplication工具类里。先定义核心启动注解Target(ElementType.TYPE) Retention(RetentionPolicy.RUNTIME) public interface NanoBootApplication { String[] scanPackages(); int port() default 8080; }标注这个注解的主类就在main方法里调用启动器public static void run(Class? primaryClass, String[] args) throws Exception { NanoBootApplication config primaryClass.getDeclaredAnnotation(NanoBootApplication.class); NanoContext context new NanoContext(config.scanPackages()); context.initialize(); HandlerRegistry registry new HandlerRegistry(context); if (shouldStartWeb(config)) { HttpServer server HttpServer.create(new InetSocketAddress(config.port()), 0); server.createContext(/, new NanoDispatcher(registry, context)); server.setExecutor(Executors.newFixedThreadPool( Math.max(4, Runtime.getRuntime().availableProcessors() * 2))); server.start(); System.out.println([NanoBoot] started on port config.port() , scanPackages String.join(,, config.scanPackages())); } }shouldStartWeb的逻辑是检查类路径里是否存在io.nanoboot.web.NanoDispatcher需要说明的是这一步用的是存在性判断不需要加载它就能决定要不要尝试启动HTTP Server这样任务型应用不引入web模块时不会因为缺少类而报错。这个设计有实用价值NanoBoot的core模块脱离web模块单独使用完全合法。NanoContext.initialize()内部只做四件事解析扫描包列表扫描class文件实例化组件依赖注入。整个初始化过程被设计成幂等的重复调用会直接抛IllegalStateException避免开发者误操作。3.3 组件扫描与依赖注入代码拆解核心扫描逻辑占用了一个类ClassScanner其核心方法是递归扫描目录public SetClass? scan(PackageScope root) throws IOException { SetClass? classes new HashSet(); String basePath root.getPackageName().replace(., /); EnumerationURL urls Thread.currentThread() .getContextClassLoader().getResources(basePath); while (urls.hasMoreElements()) { URL url urls.nextElement(); if (!file.equals(url.getProtocol())) continue; File dir new File(url.toURI()); collectClasses(dir, root.getPackageName(), classes); } return classes; }collectClasses方法里对目录下的每个文件做判断文件名以.class结尾且不包含$符号排除内部类和匿名类就用Class.forName(className, false, classLoader)加载。这里第二参数用false的目的很讲究它只加载类不初始化静态块避免扫描阶段触发用户代码的静态逻辑导致依赖还没注入完成就执行了业务代码。依赖注入使用字段遍历赋值实现版本一上来就选择了最直接的反射路径。NanoContext里维护一个MapClass?, Object beanPool注册Bean时通过类上的NanoComponent识别。注入时遍历所有字段for (Field field : clazz.getDeclaredFields()) { if (!field.isAnnotationPresent(NanoAutowired.class)) continue; Object dependency resolveDependency(field.getType()); field.setAccessible(true); field.set(instance, dependency); }我特别强调了一个细节所有setAccessible(true)必须在初始化阶段完成不要在请求处理阶段临时设置。因为setAccessible本身有性能开销而且在高版本JDK中如果模块没有开放可能在设置时抛出InaccessibleObjectException。NanoBoot在初始化阶段统一做权限处理运行期就完全没有反射权限问题。3.4 路由注册与分发器的具体实现路由注册在Web模块里实现。核心是遍历容器里所有Bean看类上是否标了NanoController标了就处理这个类的每个公开方法for (Method method : clazz.getDeclaredMethods()) { NanoMapping mapping method.getAnnotation(NanoMapping.class); if (mapping null) continue; String route buildRoute(mapping.path(), mapping.method()); ParameterBinder binder new ParameterBinder(method, bean); HandlerMeta meta new HandlerMeta(bean, method, binder); registry.register(route, meta); }ParameterBinder这个类负责把HttpExchange里的参数转换成方法调用需要的实参数组。核心逻辑是先获取方法参数注解有NanoParam就用指定的参数名去请求参数Map里取没有注解就用参数名直取然后调用预先注册好的类型转换器IntegerConverter、LongConverter等完成字符串到目标类型的转换。对于没有匹配到值的参数基本类型给默认值int给0、boolean给false包装类型给null。这套规则的实现代码不多但覆盖了大多数接口开发场景。分发器NanoDispatcher的入口处理流程是构建一个交换包装对象RequestExchange从中提取请求方法、路径、参数Map交给路由表查询查不到返回404查得到就执行拦截器链然后反射调用方法并处理返回值。返回值处理分三类情况如果被NanoResponseBody(convert json)标注用JSON序列化器输出如果返回void或null响应状态设为204其他情况用toString输出纯文本。3.5 最小可用Demo与完整运行验证写完核心之后我用一个完整的接口工程验证了整条链路。主类内容如下NanoBootApplication(scanPackages com.example.demo, port 9090) public class DemoApplication { public static void main(String[] args) throws Exception { NanoBootApplication.run(DemoApplication.class, args); } }再写一个Controller和ServiceNanoComponent public class UserService { public String greet(String name) { return Hello, name; } } NanoController public class UserController { NanoAutowired private UserService userService; NanoMapping(path /hello, method GET) public String hello(NanoParam(name) String name) { return userService.greet(name); } }引入Web模块后运行main方法控制台输出[NanoBoot] started on port 9090。然后我执行curl http://127.0.0.1:9090/hello?namenanoboot返回了Hello, nanoboot。这里有个小细节因为返回时候没有标NanoResponseBody用的是纯文本输出所以响应头的Content-Type为text/plain; charsetutf-8如果方法被标注了JSON转换则会输出{code:200,data:Hello, nanoboot}这样的结构化对象。跑通第一个Demo时的成就感很强但我也顺手验证了各种边界访问不存在的路径返回404请求参数缺失且参数类型是int时返回0把参数名写错时返回默认值而不是异常——这些都是框架约定好了的写代码时心里有数很重要。4. 性能量化与对比测试分析4.1 测试环境与测量方法NanoBoot的性能数据是在一台Linux服务器上测的配置为4核8G、JDK 11.0.18测试前先预热JVM并关闭自适应内存调整用-Xms64m -Xmx64m固定堆大小。对比对象是同样环境的SpringBoot 2.7.14空Web项目仅引入spring-boot-starter-web和NanoBoot空项目。测量指标有三项启动耗时从main方法进入后System.nanoTime到端口可接受请求的耗时、堆内存占用用JMX MemoryMXBean获取初始化完成后的heapUsed、Jar大小Maven打出的fat-jar大小。为了保证对比公平两边都没有做任何额外配置SpringBoot不关闭JSP、不排除数据库自动配置也就是说对比的是“开箱即用”状态。需要说明这个对比不是要碾压SpringBoot而是想给读者一个量化认知框架启动需要付出多少代价。SpringBoot的价值在于出色的生态整合能力但它的基础成本确实也不容忽视。4.2 关键性能指标对比表指标NanoBootSpringBoot 2.7.14差异说明启动耗时约85ms约1.6s快18倍左右堆内存占用约28MB约260MB低约9倍Jar包大小15KB依赖JDK16.8MB体积差3个数量级依赖数量0JDK自带20个三方包可维护性差异明显线程模型固定线程池默认8线程内嵌Tomcat自适应线程高并发场景需求匹配差异这个表非常直观地反映了NanoBoot的定位它不可能替代SpringBoot更不可能在复杂业务场景里扳手腕但在“启动速度敏感”“运行内存受限”“部署包需要极简”这三类场景里NanoBoot的优势是压倒性的。说到高并发我补充一组压测数据同样这台机器上用wrk对/hello接口压测NanoBoot在30秒内扛住了约2.1万QPS99分位延迟在18ms左右SpringBoot在同样场景下约为3.5万QPS99分位延迟是12ms。这个差距主要来自JDK HttpServer没有Tomcat那双层连接器缓冲以及NIO优化的加成但对绝大多数中小应用来说2万QPS已经绰绰有余。4.3 什么时候选NanoBoot什么时候不选结合上面的量化数据我把选型建议说得很直白如果你的项目需要Spring生态里的JPA、Security、Cloud Gateway、Actuator、配置中心或者你团队所有人只写过Spring那直接用SpringBoot别纠结轻量不轻量。但如果你做的是嵌入式设备上的采集程序、容器里启动次数频繁的Sidecar服务、对内API网关、学习用的小项目NanoBoot这类轻量框架会更匹配。还有一种情况很常见一个大系统由多个子服务组成边缘模块用NanoBoot搭核心业务用SpringBoot搭。两个模块通过HTTP或消息队列通信轻量模块冷启动快、内存小可以在容器调度中快速伸缩核心模块负责复杂逻辑享有完整生态。这个混合架构我实际跑过效果不错不算天马行空。5. 常见问题与排查技巧实录5.1 明明标了NanoComponent却扫描不到这个问题八成出在扫描路径写错了。NanoBootApplication(scanPackages com.example.demo)里的包名必须和Controller实际所在包一致不能只写com.example而实际类在com.example.demo.controller子包里但有一个前提——NanoBoot的扫描是递归的写com.example也会扫到com.example.demo.controller。真正导致扫不到的原因通常是三类类是package-private的反射实例化时无法访问构造函数类名里含$符号被过滤类是用NanoController标记但忘了写NanoComponent——NanoBoot规定Controller本身也是个组件必须两个注解都标注。排查方法很简单在NanoContext.initialize()里加一行临时日志输出已注册的Bean Class名单。NanoBoot的扫描器还提供了debug()开关开启后会打印每个被跳过类的跳过原因这个功能调试初期帮我节省了大量时间。5.2 依赖注入时出现NullPointerException如果容器已经完成初始化但某个Service在Controller里还是null十有八九是依赖注入顺序出了问题。NanoBoot的注入策略是两遍遍历先创建所有实例再统一注入。所以理论上Controller和Service应该在注入阶段都能拿到依赖。但有一个坑如果Service类上没有NanoComponent它的实例根本不会被创建Controller里的NanoAutowired字段自然就是null而且框架只会报“无法解析依赖类型”不会告诉你“你忘记在那个类上加注解了”。检查思路反着来找出报错类型对应的实现类确认它的类上是否标注了组件注解以及构造函数是否为public无参。另外有个细节NanoBoot的容器如果遇到循环依赖比如A注入B、B注入A会直接抛CircularDependencyException。我当时设计成先创建一个空列表记录正在实例化的类名再次进入设定路径时立即报错。这样虽然不能在运行时支持循环依赖但至少不会像Spring那样出现奇怪的初始化顺序问题。5.3 请求返回404排查全流程路由404的第一反应是查路由表。在NanoDispatcher入口加一行System.out.println(Request: method path)再查注册时的route key对比是不是路径拼错。常见问题有HTTP方法不匹配注解写的是POST请求用的是GET路由路径大小写不一致Controller方法是非public的因为NanoMapping只识别public方法方法参数列表里包含HttpExchange或HttpRequest这类框架内置参数类型而框架在绑定请求参数时发现类型对不上就静默跳过了。还有一个实际遇到过的坑路径末尾的斜杠问题。/hello和/hello/在NanoBoot里被当作两个不同路径但如果请求路径以/结尾而路由表里没有分发器会做一次“去末尾斜杠重试”的逻辑但只在路由表里没有精确匹配时才执行。这个设计是为了兼容一些客户端自动加斜杠的行为但也意味着如果你的路由定义本身就是/hello/而请求是/hello会404。建议统一风格定义路由时不要带末尾斜杠。5.4 并发请求下数据错乱与线程安全JDK HttpServer本身是多线程的NanoBoot的默认线程数是cpu*2如果同时来1000个请求线程池会排队但响应不会错。真正需要防的是你写Controller时自己留了共享可变全局变量。比如在一个类里放了一个Map缓存数据多个线程同时读写就可能出现可见性问题。NanoBoot不会替你处理线程安全它只提供synchronized不了的代码约定Controller和Service默认都是单例所以类里不应该保存“请求级别的状态”。如果确实需要在请求间传递用户上下文不要在Controller里加个ThreadLocal就完事——因为NanoBoot的分发线程不能保证同一个用户的请求始终落在同一个线程上。正确做法是定义一个请求上下文对象在拦截器里从HttpExchange取header并写入一个封装在RequestExchange里的MapController直接从参数里接收。这也是我后期演进NanoBoot时最重要的经验之一。5.5 高版本JDK反射限制导致启动失败JDK 16之后强封装了Java内部APINanoBoot如果扫描到非公开包的类并调用setAccessible(true)可能会抛出InaccessibleObjectException。遇到这个问题时先在启动命令加JVM参数java --add-opens java.base/java.langALL-UNNAMED \ --add-opens java.base/java.utilALL-UNNAMED \ -jar nanoboot-demo.jar不过后来我改进了处理策略NanoBoot的默认行为是“能注入就注入不能注入就给用户一个明确的错误而不是JVM那种晦涩的内部异常”。如果你在运行期看到InaccessibleObjectException第一反应不是加透传参数而是检查是否扫描了不该扫的包比如直接扫了java.util这种JDK内部包的路径。正常情况下NanoBoot只扫你应用自己的业务包完全不会碰到强封装边界。最后分享两个小设计心得写NanoBoot的整个过程让我重新理解了一个被很多教程忽略的事实框架的本质不是魔法而是把“扫描、反射、代理、约定”这些Java基础能力组织成一套可复用的启动流程。你写完了NanoBoot再去读SpringBoot源码会发现它的核心原理完全相通只是多了一堆自动配置和条件装配的外壳。如果你打算自己动手写一个类似的迷你框架我的建议是从最小点切入先写一个只能注册和获取Bean的容器再往里加一个路由注册器最后才考虑HTTP入口。每加一层就写对应的测试用例否则一旦出现问题你根本分不清是容器没初始化好还是参数绑定器写错了。把今天这篇文章里的代码按序跑通后再自己加一个AOP链或者定时任务模块你对Java反射的掌握程度会比背十道面试题有用得多。