手写迷你版Spring Boot:彻底搞懂Spring Boot底层运行机制

手写迷你版Spring Boot:彻底搞懂Spring Boot底层运行机制 我敢说80%的Java程序员用了很久Spring Boot都不知道它底层到底是咋跑起来的为什么写个main方法加个SpringBootApplication注解就能启动一个Web服务为什么不用打WAR包放到Tomcat里就能跑我之前也是懵的直到去年闲得慌跟着Spring Boot的源码手写了一个简化版的迷你Spring Boot只实现了四个核心功能代码加起来不到500行却彻底搞懂了Spring Boot的底层运行逻辑。今天就把这个手写的过程分享给大家只要你有基础的Spring和Java Web基础就能跟着一步一步复现写完你会发现哦原来Spring Boot的核心原理这么简单一、项目准备1.1 需求梳理我们要实现的迷你Spring Boot只保留四个最核心的功能自动配置扫描扫描指定包下的组件自动注册到Bean容器依赖注入简化实现简单的依赖注入功能请求路由映射实现Controller的路径映射处理HTTP请求内嵌Tomcat启动不需要外部Tomcat启动main方法就能运行Web服务1.2 环境依赖JDK 17Maven 3.6只需要三个依赖spring-contextSpring核心容器tomcat-embed-core内嵌Tomcatjakarta.servlet-apiServlet API1.3 项目结构mini-spring-boot ├── pom.xml └── src └── main └── java └── com └── weige └── minispringboot ├── annotation │ ├── MyAutowired.java │ ├── MyGetMapping.java │ ├── MyRestController.java │ └── MySpringBootApplication.java ├── core │ ├── MiniSpringApplication.java │ └── BeanContainer.java └── web ├── DispatcherServlet.java └── HandlerMapping.java二、第一步实现核心注解首先我们把需要用到的注解先定义好和Spring Boot的注解对应上方便理解。2.1 MySpringBootApplication 启动类注解对应Spring的SpringBootApplication标记启动类包含包扫描功能Target(ElementType.TYPE)Retention(RetentionPolicy.RUNTIME)publicinterfaceMySpringBootApplication{// 扫描的包路径默认是启动类所在包String[]basePackages()default{};}2.2 MyRestController Controller注解对应Spring的RestController标记这是一个处理HTTP请求的ControllerTarget(ElementType.TYPE)Retention(RetentionPolicy.RUNTIME)Component// 继承Component会被扫描到容器里publicinterfaceMyRestController{}2.3 MyGetMapping 路由注解对应Spring的GetMapping标记方法的GET请求路径Target(ElementType.METHOD)Retention(RetentionPolicy.RUNTIME)publicinterfaceMyGetMapping{// 请求路径Stringvalue();}2.4 MyAutowired 依赖注入注解对应Spring的Autowired实现自动注入Target(ElementType.FIELD)Retention(RetentionPolicy.RUNTIME)publicinterfaceMyAutowired{}三、第二步实现Bean容器和依赖注入接下来我们实现一个简单的Bean容器负责扫描组件、创建Bean、实现依赖注入。3.1 Bean容器单例类publicclassBeanContainer{// 存储所有Beankey是类名首字母小写value是Bean实例privatestaticfinalMapString,ObjectbeanMapnewConcurrentHashMap();privateBeanContainer(){}// 获取单例实例publicstaticBeanContainergetInstance(){returnHolder.INSTANCE;}privatestaticclassHolder{privatestaticfinalBeanContainerINSTANCEnewBeanContainer();}// 添加BeanpublicvoidaddBean(StringbeanName,Objectbean){beanMap.put(beanName,bean);}// 根据名称获取BeanpublicObjectgetBean(StringbeanName){returnbeanMap.get(beanName);}// 根据类型获取BeanpublicTTgetBean(ClassTclazz){for(Objectbean:beanMap.values()){if(clazz.isInstance(bean)){returnclazz.cast(bean);}}returnnull;}// 获取所有BeanpublicMapString,ObjectgetAllBeans(){returnCollections.unmodifiableMap(beanMap);}}3.2 实现包扫描和Bean初始化我们写一个方法扫描指定包下所有带Component注解的类创建实例放到容器里然后处理MyAutowired注解注入依赖privatevoidscanAndInitBeans(StringbasePackage)throwsException{// 把包路径转换成文件路径StringpathbasePackage.replace(.,/);EnumerationURLresourcesThread.currentThread().getContextClassLoader().getResources(path);while(resources.hasMoreElements()){URLresourceresources.nextElement();FiledirnewFile(resource.getFile());// 遍历目录下所有class文件for(Filefile:dir.listFiles(f-f.getName().endsWith(.class)||f.isDirectory())){if(file.isDirectory()){// 递归扫描子包scanAndInitBeans(basePackage.file.getName());}else{// 加载类StringclassNamebasePackage.file.getName().replace(.class,);Class?clazzClass.forName(className);// 如果类带Component注解包括子注解比如MyRestControllerif(clazz.isAnnotationPresent(Component.class)){// 创建实例Objectbeanclazz.getDeclaredConstructor().newInstance();// Bean名称默认是类名首字母小写StringbeanNameIntrospector.decapitalize(clazz.getSimpleName());// 放到容器里BeanContainer.getInstance().addBean(beanName,bean);}}}}// 处理依赖注入for(Objectbean:BeanContainer.getInstance().getAllBeans().values()){Field[]fieldsbean.getClass().getDeclaredFields();for(Fieldfield:fields){if(field.isAnnotationPresent(MyAutowired.class)){// 根据类型获取依赖的BeanObjectdependencyBeanContainer.getInstance().getBean(field.getType());if(dependency!null){field.setAccessible(true);field.set(bean,dependency);}}}}}这里简化了很多逻辑比如没有处理构造器注入、没有处理循环依赖、没有Bean的生命周期但是核心的扫描和注入功能已经实现了和Spring的核心逻辑是一样的。四、第三步实现请求路由映射接下来我们要处理HTTP请求的路由把路径和对应的Controller方法关联起来。4.1 HandlerMapping 路由存储类存储请求路径和对应的方法、Controller实例publicclassHandlerMapping{// key是请求路径value是对应的方法和Controller实例privatestaticfinalMapString,MethodhandlerMapnewHashMap();privatestaticfinalMapString,ObjectcontrollerMapnewHashMap();publicstaticvoidaddHandler(Stringpath,Methodmethod,Objectcontroller){handlerMap.put(path,method);controllerMap.put(path,controller);}publicstaticMethodgetMethod(Stringpath){returnhandlerMap.get(path);}publicstaticObjectgetController(Stringpath){returncontrollerMap.get(path);}}4.2 扫描路由注解在Bean初始化完成之后我们扫描所有带MyRestController注解的Bean把里面带MyGetMapping的方法注册到HandlerMapping里privatevoidinitHandlerMapping(){MapString,ObjectbeansBeanContainer.getInstance().getAllBeans();for(Objectbean:beans.values()){Class?clazzbean.getClass();if(clazz.isAnnotationPresent(MyRestController.class)){Method[]methodsclazz.getDeclaredMethods();for(Methodmethod:methods){if(method.isAnnotationPresent(MyGetMapping.class)){MyGetMappingannotationmethod.getAnnotation(MyGetMapping.class);Stringpathannotation.value();HandlerMapping.addHandler(path,method,bean);}}}}}五、第四步实现内嵌Tomcat和DispatcherServlet现在到了最核心的部分启动内嵌Tomcat处理HTTP请求。5.1 自定义DispatcherServlet这个Servlet是所有请求的入口收到请求之后找到对应的Controller方法执行返回结果publicclassDispatcherServletextendsHttpServlet{OverrideprotectedvoiddoGet(HttpServletRequestreq,HttpServletResponseresp)throwsServletException,IOException{resp.setContentType(application/json;charsetutf-8);Stringpathreq.getRequestURI();// 根据路径找到对应的方法和ControllerMethodmethodHandlerMapping.getMethod(path);ObjectcontrollerHandlerMapping.getController(path);if(methodnull||controllernull){resp.setStatus(HttpServletResponse.SC_NOT_FOUND);resp.getWriter().write(404 Not Found);return;}try{// 执行方法Objectresultmethod.invoke(controller);// 把结果转成JSON返回这里简化直接toString实际项目可以用Jacksonresp.getWriter().write(result.toString());}catch(Exceptione){e.printStackTrace();resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);resp.getWriter().write(500 Internal Server Error);}}}5.2 启动内嵌Tomcat写一个方法启动Tomcat注册我们的DispatcherServletprivatevoidstartTomcat(intport)throwsException{// 创建Tomcat实例TomcattomcatnewTomcat();// 设置端口tomcat.setPort(port);// 设置临时目录tomcat.setBaseDir(System.getProperty(java.io.tmpdir));// 添加ContextContextcontexttomcat.addContext(,null);// 注册DispatcherServletTomcat.addServlet(context,dispatcherServlet,newDispatcherServlet());// 映射所有请求到DispatcherServletcontext.addServletMappingDecoded(/*,dispatcherServlet);// 启动Tomcattomcat.start();System.out.println(Tomcat started on port: port);System.out.println(Mini Spring Boot started successfully!);// 阻塞等待请求tomcat.getServer().await();}六、第五步封装启动类MiniSpringApplication把上面的所有步骤封装到一个启动类里提供静态run方法和Spring Boot的SpringApplication.run对应publicclassMiniSpringApplication{publicstaticvoidrun(Class?primarySource,String[]args)throwsException{// 1. 解析启动类上的MySpringBootApplication注解MySpringBootApplicationannotationprimarySource.getAnnotation(MySpringBootApplication.class);StringbasePackageannotation.basePackages().length0?annotation.basePackages()[0]:primarySource.getPackageName();System.out.println(Starting Mini Spring Boot...);System.out.println(Scanning base package: basePackage);// 2. 扫描包初始化Bean实现依赖注入BeanContainercontainerBeanContainer.getInstance();scanAndInitBeans(basePackage);// 3. 初始化路由映射initHandlerMapping();// 4. 启动内嵌Tomcat默认端口8080startTomcat(8080);}// 上面的scanAndInitBeans、initHandlerMapping、startTomcat方法都放这里}七、测试运行Demo现在我们来写一个测试项目验证我们的迷你Spring Boot能不能正常工作。7.1 写一个ServiceComponentpublicclassHelloService{publicStringsayHello(Stringname){returnHello name, Welcome to Mini Spring Boot!;}}7.2 写一个ControllerMyRestControllerpublicclassHelloController{MyAutowiredprivateHelloServicehelloService;MyGetMapping(/hello)publicStringhello(){returnhelloService.sayHello(Weige);}MyGetMapping(/test)publicStringtest(){returnTest Success!;}}7.3 写启动类MySpringBootApplicationpublicclassDemoApplication{publicstaticvoidmain(String[]args)throwsException{MiniSpringApplication.run(DemoApplication.class,args);}}7.4 运行测试直接运行DemoApplication的main方法控制台输出Starting Mini Spring Boot... Scanning base package: com.weige.demo Tomcat started on port: 8080 Mini Spring Boot started successfully!打开浏览器访问http://localhost:8080/hello返回Hello Weige, Welcome to Mini Spring Boot!访问http://localhost:8080/test返回Test Success!访问不存在的路径http://localhost:8080/aaa返回404 Not Found完美我们的迷你Spring Boot已经可以正常工作了八、原理总结你看我们只写了不到500行代码就实现了Spring Boot的核心功能其实Spring Boot的底层原理就是这么简单启动的时候扫描指定包下的组件注册到Bean容器实现依赖注入把Bean之间的依赖关系处理好扫描Controller的路由映射保存路径和方法的对应关系启动内嵌Tomcat所有请求交给DispatcherServlet处理DispatcherServlet根据请求路径找到对应的方法执行返回结果当然真正的Spring Boot要复杂得多它还处理了各种自动配置按需加载Bean的生命周期、循环依赖处理各种类型的参数解析、返回值处理异常处理、拦截器、过滤器多环境配置、配置文件加载等等等等但是核心逻辑和我们写的迷你版是一模一样的搞懂了这个简化版再去看Spring Boot的源码就会轻松很多。九、考点提炼面试的时候如果被问到Spring Boot的底层原理你可以这么说Spring Boot启动的时候先解析SpringBootApplication注解获取要扫描的包路径扫描包下所有带Component注解的类创建实例放到IOC容器里处理依赖注入扫描所有Controller类的路由注解