ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

手写迷你SpringBoot:启动流程、自动装配与内嵌Web服务器核心原理

手写迷你SpringBoot:启动流程、自动装配与内嵌Web服务器核心原理 手写SpringBoot核心流程这件事我其实惦记了很久。用了好几年的SpringBoot每次看到那个SpringBoot的启动图标刷出来心里总有个声音在问它到底在main方法那一行里做了多少事为什么一个空的Spring Boot项目就能把内嵌Tomcat跑起来为什么我写一个带Configuration的类什么都不配就能被自动扫描到这些问题的答案网上讲自动装配原理的帖子很多但大多停留在看源码画流程图的层面。作为程序员光看还是会心虚真正的理解得靠动手写出来。所以这篇文章我决定带大家从零手写一个迷你版SpringBoot不依赖Spring Boot本身的任何能力只基于最基础的Spring容器和JDK把启动流程、自动装配、内嵌Web服务器这三条主线全部自己实现一遍。看完并跟着写完之后你对SpringBoot自动装配和启动原理的理解会比刷十篇源码分析都扎实。1. 手写前的整体设计一个迷你SpringBoot该有哪些零件1.1 先明确我们要实现的三个核心能力在动手写代码之前得先想清楚手写的边界。我们不可能也没有必要把SpringBoot几万个类都写一遍关键是复刻它的核心运行逻辑。我给自己定的目标是实现以下三个能力第一个启动能力。也就是通过一个main方法调用我们自己的MySpringApplication.run(启动类.class)能把整个应用拉起来。这里面包含主类扫描、容器初始化、Bean扫描注册、Web服务器启动。第二个自动配置能力。这是SpringBoot最核心的卖点。我们要实现一个自己的MyEnableAutoConfiguration注解配合一个类似于spring.factories的配置文件让容器启动时能自动加载一组约定好的配置类并且能根据条件判断这类配置到底生不生效比如你引入了某个依赖配置才生效。第三个内嵌Web容器能力。SpringBoot最惊艳的地方就是把Tomcat嵌进来了package成了一个可执行Jar。我们可以用JDK内置的com.sun.net.httpserver.HttpServer或者为了体验更真实引入一个极简的嵌入式Http服务器实现请求的接收和HTTP响应。为了让这个项目有实际价值我选取的场景是写一个微型的HTTP接口服务通过我们自己的框架启动访问/api/test能返回一段JSON数据。这个场景覆盖了配置类扫描、Bean创建、依赖注入、请求路由这几个关键环节能完整验证框架是否跑通。1.2 技术选型Maven工程和基础依赖这个练习项目基于Maven构建JDK要求8以上如果本地是JDK 17也没问题。核心依赖只需要两个dependency groupIdorg.springframework/groupId artifactIdspring-context/artifactId version5.3.30/version /dependency再加一个Servlet API因为我们的Web服务器要处理HTTP请求写成标准Servlet接口会更接近真实SpringBoot的做法dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency注意这里用provided作用域是因为Servlet API由服务器容器提供我们不能把接口实现类打包进去否则容器类加载时会冲突。有同学会问为什么不多引入几个Spring模块原因很简单——我们就是要用最瘦的Spring环境让所有扩展能力都出自自己的代码。Spring容器本身只提供Bean定义、实例化、依赖注入这些基础能力剩下的启动编排、自动配置、Web集成全部手写这样才说得上是真正的拆解和重组。1.3 包的总体结构规划写代码之前先规划好包结构这会直接影响我们后续对启动流程的理解。真实的SpringBoot代码里核心类分布在spring-boot.jar里我参考它的结构做一个简化版com.myboot ├── MySpringApplication.java // 启动门面对应SpringApplication ├── MySpringBootApplication.java // 启动注解对应SpringBootApplication ├── MyEnableAutoConfiguration.java // 自动配置开关注解 ├── MyConditionalOnClass.java // 条件注解对应ConditionalOnClass ├── MyAutoConfigurationImportSelector.java // 配置加载器对应AutoConfigurationImportSelector └── web ├── MyTomcatServer.java // 内嵌HTTP服务器启动器 ├── MyDispatcherServlet.java // 请求分发器 └── MyRequestMapping.java // 路由注解这个结构已经能看出门道了启动类负责整体编排注解负责声明Server负责生命周期DispatcherServlet负责请求分发。跟着这个结构一层层实现整个流程就被带出来了。2. 启动流程底层设计从一句run开始2.1 为什么SpringBoot能用一个main函数启动全部很多人第一次用SpringBoot都有个疑惑为什么一个main函数能启动一个Web应用做传统Web开发的时候我们都是先把WAR包丢进Tomcat然后由Tomcat调用我们写的类。SpringBoot完全反过来了它是在main函数里主动启动容器再在容器内部启动Tomcat。这就好比两种开饭店的方式传统模式是你租好店面装好Tomcat然后请厨师你的WAR包入驻SpringBoot模式是你带着厨师团队应用代码Spring容器自己把店面支起来内嵌Tomcat想在哪开在哪开。这个转变是所有后续理解的基础。实现上run方法要做的事情就明确了创建容器、注册主配置类、扫描Bean、刷新容器、启动内嵌Web服务器、完成收尾。下面我给出代码实现。2.2 自己动手写run方法的主体流程先看启动门面类MySpringApplication它对应真实框架的SpringApplication类public class MySpringApplication { public static void run(Class? primarySource, String... args) { // 1. 打印启动Banner printBanner(); // 2. 创建Spring容器 AnnotationConfigApplicationContext context new AnnotationConfigApplicationContext(); // 3. 注册主配置类 context.register(primarySource); // 4. 刷新容器触发Bean扫描、自动配置、Bean创建 context.refresh(); // 5. 启动内嵌Web服务器 MyTomcatServer server context.getBean(MyTomcatServer.class); server.start(); // 6. 注册关闭钩子 Runtime.getRuntime().addShutdownHook(new Thread(context::close)); } private static void printBanner() { System.out.println(); System.out.println( MyBoot Starting ...); System.out.println(); } }这段代码看起来简单但里面的每一步都有讲究。选AnnotationConfigApplicationContext而不选别的容器是因为它既支持注解配置也能在创建之后手动注册配置类这正好契合SpringBoot拿到主类再初始化容器的模式。context.refresh()这一步是Spring容器的核心方法这一调用的背后会执行BeanFactory的准备工作、BeanDefinition的注册、单例Bean的实例化等十二大步生命周期。2.3 主配置类和组件扫描是怎么被处理的上面直接context.register(primarySource)还不够因为我们希望启动类所在的包及其子包都能被扫描到。SpringBoot实际上是通过启动类上的SpringBootApplication包装了ComponentScan并且默认扫描启动类所在包。手写的时候需要在容器刷新前手动指定扫描路径public class MySpringApplication { public static void run(Class? primarySource, String... args) { printBanner(); AnnotationConfigApplicationContext context new AnnotationConfigApplicationContext(); // 注册主配置类 context.register(primarySource); // 手动设置扫描包路径以主类所在包为根 String basePackage primarySource.getPackage().getName(); context.scan(basePackage); // 刷新容器触发完整生命周期 context.refresh(); MyTomcatServer server context.getBean(MyTomcatServer.class); server.start(); Runtime.getRuntime().addShutdownHook(new Thread(context::close)); } }这里有个细节值得注意context.scan(basePackage)和context.register(primarySource)是两码事。register是把主配置类当作一个显式注册的配置类容器会处理它的注解scan则是通过路径去扫描该包下的所有Component、Service、Configuration等标注的类。如果只register而不scan启动类之外自己写的Controller就统统不会被发现这点和真实SpringBoot行为是一致的——你的启动类必须放在项目包的根路径就是为了让扫描范围覆盖到所有子包。2.4 为什么容器启动要集中在一次refresh里完成Spring容器的refresh()方法我在这里特意单独强调因为这是理解启动原理的关键中的关键。很多人在看源码时看见refresh就头疼里面十几个方法其实我们只需要理解它的设计意图它把容器的生命周期做了一个统一编排包括准备BeanFactory、调用BeanFactory后置处理器、注册Bean后置处理器、初始化消息源、初始化事件广播器、执行所有BeanFactory后置处理器、注册监听器、实例化所有非懒加载单例Bean最后完成刷新。真实SpringBoot虽然引入了WebServerApplicationContext等更多容器类型但核心依然是执行AbstractApplicationContext.refresh()。所以面试的时候如果有人问你SpringBoot启动流程你完全可以从refresh展开初始化容器环境、扫描配置类、执行自动配置、实例化Bean、启动Web容器这样回答既系统又有深度。手写一遍之后你背流程不再是死记硬背而是能画着图讲给别人听。3. 自动装配机制实现逐行拆解核心原理3.1 自动装配到底在解决什么问题进入自动装配之前先想清楚一个根本问题为什么我们需要自动装配没有它的时代我们引入一个RedisTemplate就得自己写Bean方法创建连接工厂、创建模板、设置序列化器任何一个Spring整合的库用户都要自己写一堆配置类。SpringBoot要做的是消灭这部分样板代码让用户引入一个依赖框架自动把需要的Bean准备好用户拿来即用。那框架怎么知道该装配哪些Bean这就要靠约定依赖的Jar包自带配置信息框架启动时扫描这些信息然后根据当前项目的实际情况决定是否创建对应的Bean。这句听起来简单实现上需要三个零件缺一不可一个开关注解EnableAutoConfiguration告诉框架要启用自动装配。一份配置文件spring.factories列出所有可能生效的自动配置类。一组条件判断ConditionalOnXxx让配置类具备按需生效的能力。3.2 自己实现MyEnableAutoConfiguration注解我们写一个自己的开关注解用法和EnableAutoConfiguration保持一致Target(ElementType.TYPE) Retention(RetentionPolicy.RUNTIME) Documented Inherited Import(MyAutoConfigurationImportSelector.class) public interface MyEnableAutoConfiguration { }注意看Import(MyAutoConfigurationImportSelector.class)这一行。Import是Spring提供的一个非常强大的功能它可以导入一个普通的配置类也可以导入一个ImportSelector实现类。如果导入的是ImportSelectorSpring会调用它的selectImports方法根据返回值把一批类名注册为BeanDefinition。这正是自动装配得以实现的关键通道配置类不是用户自己写在代码里的而是SelectSelector从配置文件中批量读出来的。这种间接层的设计非常优雅启动类上只写了一个注解实际却被注入了成百上千个候选配置类。3.3 手写配置加载器模拟读取spring.factories和条件判断接着实现MyAutoConfigurationImportSelector。在真实SpringBoot中这个类会读取META-INF/spring.factories或者是新版本的AutoConfiguration.imports文件拿到所有自动配置类的类名然后逐一做条件过滤最后返回真正需要注册的类名列表。我们简化一点但流程保持完整public class MyAutoConfigurationImportSelector implements ImportSelector { Override public String[] selectImports(AnnotationMetadata importingClassMetadata) { // 1. 读取配置文件中声明的自动配置类 ListString configurations loadAutoConfigurations(); // 2. 按条件过滤 ListString validConfigurations new ArrayList(); for (String className : configurations) { if (matchCondition(className)) { validConfigurations.add(className); } } return validConfigurations.toArray(new String[0]); } private ListString loadAutoConfigurations() { // 模拟从META-INF/myboot.factories读取 ListString list new ArrayList(); list.add(com.myboot.config.RedisAutoConfiguration); list.add(com.myboot.config.WebMvcAutoConfiguration); list.add(com.myboot.config.TomcatAutoConfiguration); return list; } private boolean matchCondition(String className) { try { Class? clazz Class.forName(className); MyConditionalOnClass conditional clazz.getAnnotation(MyConditionalOnClass.class); if (conditional null) { return true; } for (String requiredClass : conditional.value()) { try { Class.forName(requiredClass); } catch (ClassNotFoundException e) { return false; } } return true; } catch (ClassNotFoundException e) { return false; } } }上面的写法有个小提示判断类是否存在用了Class.forName这会触发类的静态初始化实际上更稳妥的方式是用ClassLoader.loadClass。但作为练习项目Class.forName足够直观。真实的ConditionalOnClass是通过解析类元数据实现的不会真的去加载类它会用ASM去读取类的注解元数据而不触发类初始化从而避免很多奇奇怪怪的类加载副作用。这一点如果你去看源码会深有体会。3.4 条件注解MyConditionalOnClass和两个示例配置类接着写条件注解它是自动配置按需生效的核心决策器Target(ElementType.TYPE) Retention(RetentionPolicy.RUNTIME) public interface MyConditionalOnClass { String[] value(); }然后是自动配置类本身。注意RedisAutoConfiguration上面挂的条件是RedisTemplate是否存在如果项目里没引这个类那么整个配置类就会跳过适用性判断一目了然Configuration MyConditionalOnClass(org.springframework.data.redis.core.RedisTemplate) public class RedisAutoConfiguration { Bean public MyRedisClient myRedisClient() { return new MyRedisClient(localhost, 6379); } }再来看Web相关的配置类。为了让Tomcat配置自动生效我们在加载器里声明了TomcatAutoConfiguration并且它没有条件门槛只要框架启动就生效Configuration public class TomcatAutoConfiguration { Bean public MyTomcatServer myTomcatServer() { return new MyTomcatServer(8080); } }有同学可能会疑惑明明只是启动一个内嵌服务器为什么要单独做一个自动配置类这正是为了演示自动装配的效果MyTomcatServer这个Bean不是通过Component扫描注册的而是通过MyAutoConfigurationImportSelector从配置中导入的。启动时容器先执行扫描扫描到启动类和自动配置类再通过ImportSelector读取配置列表把这些配置类注册进去。这样整个装配过程就有了真实的链路感。3.5 在自定义启动注解中组合组件扫描有了自动配置的开关之后我们要把它组合成一个总注解对应SpringBoot的SpringBootApplication。真实场景中它由SpringBootConfiguration、EnableAutoConfiguration、ComponentScan三个注解组合而成Target(ElementType.TYPE) Retention(RetentionPolicy.RUNTIME) Documented Inherited MyEnableAutoConfiguration ComponentScan public interface MySpringBootApplication { }注意组合注解里引入了ComponentScan但没有指定包路径。Spring处理这种情况时会默认使用标注了该组合注解的类所在包作为扫描包路径。比如我们的启动类写在com.demo包下那com.demo及其子包都会被扫描。这个设计我觉得特别精妙它把约定优于配置做到了极致——什么都不用配位置本身就是配置。Inherited注解也值得说一句它表示子类可以继承父类的该注解。虽然启动类通常不会搞继承但组合注解加上这个元注解能让Spring的工具类在解析时更好地识别注解层次。4. 内嵌Web服务器实现亲手拉响Tomcat4.1 内嵌服务器的启动逻辑与关闭钩子Web服务器模块我们分两步走先定义服务器类再实现请求分发。真实SpringBoot是启动一个内嵌Tomcat我们简化后直接基于JDK的HttpServer写一个MyTomcatServer。这个类要做的事情很明确在指定端口创建HttpServer。设置一个统一的处理器把请求转发给MyDispatcherServlet。启动服务器线程池。提供停止方法配合关闭钩子做优雅停机。实现代码如下public class MyTomcatServer { private final int port; private HttpServer server; public MyTomcatServer(int port) { this.port port; } public void start() { try { server HttpServer.create(new InetSocketAddress(port), 0); server.createContext(/, exchange - { try { MyDispatcherServlet dispatcher ApplicationContextHolder.getBean(MyDispatcherServlet.class); dispatcher.handle(exchange); } catch (Exception e) { e.printStackTrace(); String errorResponse {\code\:500,\message\:\Internal Server Error\}; byte[] bytes errorResponse.getBytes(StandardCharsets.UTF_8); exchange.sendResponseHeaders(500, bytes.length); exchange.getResponseBody().write(bytes); exchange.getResponseBody().close(); } }); server.setExecutor(Executors.newFixedThreadPool(10)); server.start(); System.out.println(MyTomcatServer started on port: port); } catch (IOException e) { throw new RuntimeException(启动Web服务器失败, e); } } public void stop() { if (server ! null) { server.stop(0); } } }有一个细节我特意保留了下来ApplicationContextHolder.getBean。因为HttpServer的handler是异步回调的它在任何地方执行我们需要一个静态的方式去获取容器中的Bean。写一个简单的ApplicationContextHolder在容器刷新完成后把ApplicationContext存进去这样DispatcherServlet就能在请求进来时被随时取到。这也是为什么真实SpringBoot里会有各种ContextHolder的原因——把容器与运行时环境解耦让框架内部代码不依赖注入就能拿到必要的组件。4.2 手写DispatcherServlet完成请求映射Web服务器启动之后真正干活的还是DispatcherServlet。我们给MyDispatcherServlet设计最简单的handle方法根据请求URI做路由分发调用对应的业务Bean返回JSONpublic class MyDispatcherServlet { public void handle(HttpExchange exchange) throws IOException { String path exchange.getRequestURI().getPath(); String response; int statusCode 200; if (/api/test.equals(path)) { response {\code\:0,\message\:\success\,\data\:\Hello MyBoot\}; } else if (/api/user.equals(path)) { UserService userService ApplicationContextHolder.getBean(UserService.class); response {\code\:0,\message\:\success\,\data\:\ userService.getUserName() \}; } else { response {\code\:404,\message\:\Not Found\}; statusCode 404; } byte[] bytes response.getBytes(StandardCharsets.UTF_8); exchange.getResponseHeaders().set(Content-Type, application/json;charsetUTF-8); exchange.sendResponseHeaders(statusCode, bytes.length); exchange.getResponseBody().write(bytes); exchange.getResponseBody().close(); } }这里我们能看到一个典型的分层Controller层在真实框架里是通过RequestMapping注解自动映射到HandlerMethod的我们手动if-else其实在模拟路由表。你可以看到当一个请求进来时由DispatcherServlet进行统一入口处理和分发这个模式是MVC框架的基础搞懂了它后面看SpringMVC源码的HandlerMapping就会觉得非常亲切。4.3 通过Bean注入联通业务代码与框架为了让框架显得更真实我们应该让UserService被Spring容器管理并能被注入到MyDispatcherServlet的使用链路里。先写一个业务接口和实现类public interface UserService { String getUserName(); }Service public class UserServiceImpl implements UserService { Override public String getUserName() { return 程序员老赵; } }为了让MyDispatcherServlet能拿到用户服务我们可以让它在初始化时通过构造器注入UserService。但由于我们是在自动配置类里创建MyDispatcherServlet的所以配置类可以直接注入Configuration public class WebMvcAutoConfiguration { Bean public MyDispatcherServlet myDispatcherServlet(ApplicationContext context) { MyDispatcherServlet servlet new MyDispatcherServlet(); servlet.setContext(context); return servlet; } }这种方式演示了SpringBoot一个特别重要的思想自动配置类本身也是配置类它和用户自己写的Configuration享有完全相同的地位可以互相注入后面的配置类也可以覆盖前面的配置类。MyDispatcherServlet的Bean定义里注入了ApplicationContext最终容器里所有Bean都准备好了之后你访问/api/user整个链路就是DispatcherServlet处理请求 - 通过Context获得Bean - 调用业务方法 - 返回JSON。4.4 启动时Bean的创建顺序与依赖关系内嵌Web服务器的启动时机也有讲究。我们前面在run里是刷新完容器后才去getBean(MyTomcatServer.class)这保证了一个先决条件Web服务器的Bean必须已经完成创建和依赖注入才能被取出并启动。如果过早启动服务器请求回调触发了Bean获取但那时的容器还没刷新完毕极有可能拿到空的容器或者不完整的Bean就会导致各种诡异的空指针。SpringBoot真实启动流程在WebApplicationContext中有一部分逻辑就是在刷新容器的最后阶段启动Web服务器的它把Web服务器当成容器生命周期的一部分来管理。我们的简化版本在refresh之后启动效果等价但解释起来更清楚。手写的时候不用过度纠结内部细节把先Bean后服务器的关键顺序保证住理解就到位了。5. 写一个真实Demo串联整个启动流程5.1 创建启动类和业务接口代码框架代码写完了接下来用一个真实的Demo把整个框架跑起来。首先创建一个SpringBoot风格的启动类MySpringBootApplication public class DemoApplication { public static void main(String[] args) { MySpringApplication.run(DemoApplication.class, args); } }这个类很简单只有一行run。但此刻我们心里很清楚这行run背后经历的流程创建容器、扫描包、加载自动配置、实例化Bean、启动Web服务器一条龙全部完成。我们把DemoApplication放在com.demo包下其他业务类都放在它的子包里这样能被默认扫描。再看全局配置文件我们在src/main/resources/META-INF下创建myboot.factories文件虽然上面的代码里loadAutoConfigurations暂时是硬编码列表但你可以顺手把它改成读文件方式com.myboot.config.RedisAutoConfiguration\ com.myboot.config.WebMvcAutoConfiguration,\ com.myboot.config.TomcatAutoConfiguration这里用等号连接多个类名是为了贴近真实spring.factories的Properties格式。如果你改成从文件读取加一个Properties.load就能搞定非常方便。5.2 运行过程和结果验证直接运行DemoApplication.main控制台会输出类似下面的日志 MyBoot Starting ... MyTomcatServer started on port: 8080然后我们用浏览器或者curl访问接口curl http://localhost:8080/api/test返回{code:0,message:success,data:Hello MyBoot}访问/api/user返回{code:0,message:success,data:程序员老赵}到这里整个手写框架就完全跑通了。这段输出能让你直观地感觉到SpringBoot的启动不是一个魔法而是我们可控、可追踪、可修改的一系列流程每一步都有迹可循这也正是手写练习的意义所在。5.3 与真实SpringBoot启动日志对照为了加深印象可以看一下真实SpringBoot的启动日志. ____ _ __ _ _ /\\ / ____ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | _ | _| | _ \/ _ | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) |____| .__|_| |_|_| |_\__, | / / / / |_||___//_/_/_/ :: Spring Boot :: (v2.7.0) 2024-01-15 10:23:45.123 INFO 10456 --- [ main] com.demo.DemoApplication : Starting DemoApplication ... 2024-01-15 10:23:45.456 INFO 10456 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http) 2024-01-15 10:23:45.789 INFO 10456 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path 你看真实日志里的关键点跟我们的迷你版是一一对应的初始化容器 - 启动Tomcat - 监听8080端口。不同的是真实框架在这个过程里还做了比如环境配置解析、ApplicationListener事件发布、ApplicationRunner回调执行等大量工作但主干流程是一致的。带着这个认知再去看源码或面试题你会发现自己面对那些SpringBoot启动过程有几个步骤的提问心里不慌了。6. 开发过程中的常见问题和避坑实录6.1 启动后8080端口被占用怎么办手写Web服务器最常遇到的就是端口冲突。如果本地已经跑了一个服务占用了8080启动时会报BindException: Address already in use。解决办法有两个一是换端口修改TomcatAutoConfiguration里new MyTomcatServer(8080)为其他端口比如9090。二是在启动前用命令查端口占用情况# Windows查看端口 netstat -ano | findstr 8080 # Linux/macOS查看端口 lsof -i :8080找到占用进程后可以直接结束进程也可以换端口。这个错误特别常见尤其是大家电脑上可能装了一堆中间件所以看到端口报错别慌先检查端口占用。6.2 扫描不到Bean的常见原因和排查思路手写框架时最让人崩溃的Bug就是自己写的Service类没被扫描到注入进来是null。排查思路我整理成一套顺序检查启动类所在包路径是否正确Spring扫描的根路径是主类所在包如果你的业务类不在子包里就需要自己框架支持配置额外的扫描路径真实SpringBoot也不会扫描到不相关包下的类。检查类上是否标注了正确的注解Service、Component、Configuration等Spring扫描器是按注解元信息来判断的标错了或者漏标了自然扫不到。检查容器刷新前是否执行了context.scan这一步极容易忘只register了主类但忘了scan的话自己的业务Bean全都会消失。我自己的踩坑经验是写框架练习时先写一个最简的Bean比如一个无依赖的Component在启动后立刻getBean验证确认扫描链路通了再往上加复杂度这样定位问题会快很多。6.3 双亲委派机制对Class.forName判断的影响自动装配条件判断依赖我们手写的matchCondition里面有句Class.forName(org.springframework.data.redis.core.RedisTemplate)。如果你在项目里没有引入Redis依赖这个类会抛出ClassNotFoundException然后自动跳过该配置类。这个逻辑本身很简单但真正理解它需要对JVM类加载机制有一点概念JDK的ClassLoader在加载类时是双亲委派模型系统类加载器会优先让父加载器尝试加载只有当父加载器找不到时才自己加载。这正是Class.forName能在运行时判断某个类在不在classpath里的底层原理。如果想在自动配置里判断得优雅一些不看ClassNotFoundException可以自己封装一个判断工具public final class ClassUtils { private ClassUtils() {} public static boolean isPresent(String className) { try { ClassLoader classLoader Thread.currentThread().getContextClassLoader(); classLoader.loadClass(className); return true; } catch (ClassNotFoundException e) { return false; } } }注意这里用了loadClass而没有用Class.forName区别在于后者会触发类的初始化而仔细的项目通常只需要判断类是否存在不需要触发静态代码块所以用loadClass更安全。你可以看到这只是手写框架过程中遇到的一个小点但它牵出的类加载知识也是SpringBoot条件装配的精髓之一。6.4 关闭钩子与优雅停机的实现最后说一下优雅停机。真实生产环境里直接kill -9杀进程会导致正在处理的请求被中断数据可能不一致。SpringBoot和Tomcat都有优雅停机机制会等待当前请求处理完再关闭。我们的迷你框架里Runtime.getRuntime().addShutdownHook会在JVM收到关闭信号时触发context.close()容器会依次执行Bean销毁方法Web服务器也顺带停止。如果你想让关闭更优雅可以在MyTomcatServer的stop方法上做微调设置一个stopping标志让请求分发器拒绝新请求并等待已有请求完成。不过这属于生产环境的优化练习阶段知道有这回事就行。7. 手写之后的进阶方向与源码阅读建议框架跑通了有了全链路的第一手体验这时的你已经和其他背过八股的面试者拉开差距了。如果还想深入我有几个建议的进阶方向第一个方向看真实源码时重点抓启动类和自动配置类。可以打开spring-boot-autoconfigure源码你会看到上百个自动配置类每一个都是Configuration加上各种ConditionalOnXxx条件注解。这时候你已经拥有自己的实现经验再去看那些条件注解的源码会有一种原来如此的透彻感。第二个方向研究LoadTimeWeaver和BeanFactoryPostProcessor的扩展。我们手写的框架用的是AnnotationConfigApplicationContext真实SpringBoot的Web环境用的是ServletWebServerApplicationContext它会往容器里添加Web相关的BeanFactoryPostProcessor。理解了BeanFactoryPostProcessor的机制就能明白为什么SpringBoot能这么轻巧地嵌入各种Web容器。第三个方向把自定义Starter的写法练一遍。真实项目中写一个SDK时往往要做一个xxx-spring-boot-starter里面要写spring.factories要写自动配置类要写条件注解。我们这次手写的经验正好可以迁移到自定义Starter的实现上。你可以试试把你自己的一个业务模块封装成Starter然后在另一个工程里引入依赖验证它能否被自动装配。这一步做完你对SpringBoot生态的理解就完全上升到另一个高度了。回头看写一个迷你SpringBoot重点不是代码量而是亲手推演了一整套设计思路怎么用组合注解简化使用门槛怎么用ImportSelector做配置的间接加载怎么用条件注解实现按需装配怎么把Web服务器当成一个普通Bean管理起来。这些设计思想才是SpringBoot最值钱的地方。手动实现一遍胜过高强度刷十篇源码解析。希望这篇文章能帮助你迈出那一步后续遇到启动问题或者阅读源码时你会发现自己开始有了一种我知道这里发生了什么的笃定感。
返回列表