用300行代码手写SpringBoot核心原理 _
依赖因为我们模拟实现的是SpringBoot而不是SpringMVC所以我直接在user包下定义了UserController和UserService最终我们希望能运行MyApplication中的main方法就直接启动了项目并能在浏览器中正常的访问到UserController中的某个方法。首先SpringBoot是基于的Spring所以我们要依赖Spring然后我希望我们模拟出来的SpringBoot也支持Spring MVC的那一套功能所以也要依赖Spring MVC包括Tomcat等所以在SpringBoot模块中要添加以下依赖xmldependencies dependency groupIdorg.springframework/groupId artifactIdspring-context/artifactId version5.3.18/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-web/artifactId version5.3.18/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version5.3.18/version /dependency dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version /dependency dependency groupIdorg.apache.tomcat.embed/groupId artifactIdtomcat-embed-core/artifactId version9.0.60/version /dependency /dependencies在User模块下我们进行正常的开发就行了比如先添加SpringBoot依赖xmldependencies dependency groupIdcom.seven/groupId artifactIdspringboot/artifactId version1.0-SNAPSHOT/version /dependency /dependencies然后定义相关的Controller和ServicejavaRestController public class UserController { Autowired private UserService userService; GetMapping(test) public String test(){ return userService.test(); } }核心注解和核心类我们在真正使用SpringBoot时核心会用到SpringBoot一个类和注解SpringBootApplication这个注解是加在应用启动类上的也就是main方法所在的类SpringApplication这个类中有个run()方法用来启动SpringBoot应用的所以我们也来模拟实现他们。一个SevenSpringBootApplication注解javaTarget(ElementType.TYPE) Retention(RetentionPolicy.RUNTIME) Configuration ComponentScan public interface SevenSpringBootApplication { }一个用来实现启动逻辑的SevenSpringApplication类javapublic class SevenSpringApplication { public static void run(Class clazz){ } }注意run方法需要接收一个Class类型的参数这个class是用来干嘛的等会就知道了。有了以上两者我们就可以在User的MyApplication中来使用了比如javaSevenSpringBootApplication public class MyApplication { // spring.factories public static void main(String[] args) { SevenSpringApplication.run(MyApplication.class); } }现在用来是有模有样了但中看不中用所以我们要来好好实现以下run方法中的逻辑了。run方法run方法中需要实现什么具体的逻辑呢首先我们希望run方法一旦执行完我们就能在浏览器中访问到UserController那势必在run方法中要启动Tomcat通过Tomcat就能接收到请求了。大家如果学过Spring MVC的底层原理就会知道在SpringMVC中有一个Servlet非常核心那就是DispatcherServlet这个DispatcherServlet需要绑定一个Spring容器因为DispatcherServlet接收到请求后就会从所绑定的Spring容器中找到所匹配的Controller并执行所匹配的方法。所以在run方法中我们要实现的逻辑如下创建一个Spring容器创建Tomcat对象生成DispatcherServlet对象并且和前面创建出来的Spring容器进行绑定将DispatcherServlet添加到Tomcat中启动Tomcat创建一个容器这个步骤比较简单代码如下javapublic class SevenSpringApplication { public static void run(Class clazz){ // Spring容器 AnnotationConfigWebApplicationContext applicationContext new AnnotationConfigWebApplicationContext(); applicationContext.register(clazz); applicationContext.refresh(); }我们创建的是一个AnnotationConfigWebApplicationContext容器并且把run方法传入进来的class作为容器的配置类比如在MyApplication的run方法中我们就是把MyApplication.class传入到了run方法中最终MyApplication就是所创建出来的Spring容器的配置类并且由于MyApplication类上有SevenSpringBootApplication注解而SevenSpringBootApplication注解的定义上又存在ComponentScan注解所以AnnotationConfigWebApplicationContext容器在执行refresh时就会解析MyApplication这个配置类从而发现定义了ComponentScan注解也就知道了要进行扫描只不过扫描路径为空而AnnotationConfigWebApplicationContext容器会处理这种情况如果扫描路径会空则会将MyApplication所在的包路径做为扫描路径从而就会扫描到UserService和UserController。所以Spring容器创建完之后容器内部就拥有了UserService和UserController这两个Bean。启动Tomcat我们用的是Embed-Tomcat也就是内嵌的Tomcat真正的SpringBoot中也用的是内嵌的Tomcat而对于启动内嵌的Tomcat也并不麻烦代码如下javapublic static void startTomcat(WebApplicationContext applicationContext){ Tomcat tomcat new Tomcat(); Server server tomcat.getServer(); Service service server.findService(Tomcat); Connector connector new Connector(); connector.setPort(8081); Engine engine new StandardEngine(); engine.setDefaultHost(localhost); Host host new StandardHost(); host.setName(localhost); String contextPath ; Context context new StandardContext(); context.setPath(contextPath); context.addLifecycleListener(new Tomcat.FixContextListener()); host.addChild(context); engine.addChild(host); service.setContainer(engine); service.addConnector(connector); tomcat.addServlet(contextPath, dispatcher, new DispatcherServlet(applicationContext)); context.addServletMappingDecoded(/*, dispatcher); try { tomcat.start(); } catch (LifecycleException e) { e.printStackTrace(); } }代码虽然看上去比较多但是逻辑并不复杂比如配置了Tomcat绑定的端口为8081后面向当前Tomcat中添加了DispatcherServlet并设置了一个Mapping关系最后启动其他代码则不用太过关心。而且在构造DispatcherServlet对象时传入了一个ApplicationContext对象也就是一个Spring容器就是我们前文说的DispatcherServlet对象和一个Spring容器进行绑定。接下来我们只需要在run方法中调用startTomcat即可javapublic static void run(Class clazz){ // Spring容器 AnnotationConfigWebApplicationContext applicationContext new AnnotationConfigWebApplicationContext(); applicationContext.register(clazz); applicationContext.refresh(); startTomcat(applicationContext); }实际上代码写到这一个极度精简版的SpringBoot就写出来了比如现在运行MyApplication就能正常的启动项目并能接收请求。此时你可以继续去写其他的Controller和Service了照样能正常访问到而我们的业务代码中仍然只用到了SevenSpringApplication类和SevenSpringBootApplication注解。实现Tomcat和Jetty的切换虽然我们前面已经实现了一个比较简单的SpringBoot不过我们可以继续来扩充它的功能比如现在我有这么一个需求这个需求就是我现在不想使用Tomcat了而是想要用Jetty那该怎么办我们前面代码中默认启动的是Tomcat那我现在想改成这样子如果项目中有Tomcat的依赖那就启动Tomcat如果项目中有Jetty的依赖就启动Jetty如果两者都没有则报错如果两者都有也报错这个逻辑希望SpringBoot自动帮我实现对于程序员用户而言只要在Pom文件中添加相关依赖就可以了想用Tomcat就加Tomcat依赖想用Jetty就加Jetty依赖。那SpringBoot该如何实现呢我们知道不管是Tomcat还是Jetty它们都是应用服务器或者是Servlet容器所以我们可以定义接口来表示它们这个接口叫做WebServer别问我为什么叫这个因为真正的SpringBoot源码中也叫这个。并且在这个接口中定义一个start方法javapublic interface WebServer { public void start(); }有了WebServer接口之后就针对Tomcat和Jetty提供两个实现类javapublic class TomcatWebServer implements WebServer{ Override public void start() { System.out.println(启动Tomcat); } } public class JettyWebServer implements WebServer{ Override public void start() { System.out.println(启动Jetty); } }而在SevenSpringApplication中的run方法中我们就要去获取对应的WebServer然后启动对应的webServer代码为javapublic class SevenSpringApplication { public static void run(Class clazz){ // Spring容器 AnnotationConfigWebApplicationContext applicationContext new AnnotationConfigWebApplicationContext(); applicationContext.register(clazz); applicationContext.refresh(); WebServer webServer getWebServer(applicationContext); webServer.start(); } public static WebServer getWebServer(WebApplicationContext applicationContext){ return null; } }这样我们就只需要在getWebServer方法中去判断到底该返回TomcatWebServer还是JettyWebServer。前面提到过我们希望根据项目中的依赖情况来决定到底用哪个WebServer我就直接用SpringBoot中的源码实现方式来模拟了。模拟实现条件注解首先我们得实现一个条件注解SevenConditionalOnClass对应代码如下javaTarget({ ElementType.TYPE, ElementType.METHOD }) Retention(RetentionPolicy.RUNTIME) Conditional(SevenCondition.class) public interface SevenConditionalOnClass { String value(); }注意核心为Conditional(SevenCondition.class)中的SevenCondition因为它才是真正得条件逻辑javapublic class SevenCondition implements Condition { Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { MapString, Object annotationAttributes metadata.getAnnotationAttributes(SevenConditionalOnClass.class.getName()); String className (String) annotationAttributes.get(value); try { context.getClassLoader().loadClass(className); return true; } catch (ClassNotFoundException e) { return false; } } }具体逻辑为拿到SevenConditionalOnClass中的value属性然后用类加载器进行加载如果加载到了所指定的这个类那就表示符合条件如果加载不到则表示不符合条件。模拟实现自动配置类有了条件注解我们就可以来使用它了那如何实现呢这里就要用到自动配置类的概念我们先看代码javaConfiguration public class WebServerAutoConfiguration { Bean SevenConditionalOnClass(org.apache.catalina.startup.Tomcat) public TomcatWebServer tomcatWebServer(){ return new TomcatWebServer(); } Bean SevenConditionalOnClass(org.eclipse.jetty.server.Server) public JettyWebServer jettyWebServer(){ return new JettyWebServer(); } }这个代码还是比较简单的通过一个WebServiceAutoConfiguration的Spring配置类在里面定义了两个Bean一个TomcatWebServer一个JettyWebServer不过这两个要生效的前提是符合当前所指定的条件比如只有存在org.apache.catalina.startup.Tomcat类那么才有TomcatWebServer这个Bean只有存在org.eclipse.jetty.server.Server类那么才有TomcatWebServer这个Bean并且我们只需要在SevenSpringApplication中getWebServer方法如此实现javapublic static WebServer getWebServer(WebApplicationContext applicationContext){ MapString, WebServer beansOfType applicationContext.getBeansOfType(WebServer.class); if (beansOfType.isEmpty()) { throw new NullPointerException(); } if (beansOfType.size() 1) { throw new IllegalStateException(); } return beansOfType.values().stream().findFirst().get(); }这样整体SpringBoot启动逻辑就是这样的创建一个AnnotationConfigWebApplicationContext容器解析MyApplication类然后进行扫描通过getWebServer方法从Spring容器中获取WebServer类型的Bean调用WebServer对象的start方法有了以上步骤我们还差了一个关键步骤就是Spring要能解析到WebServiceAutoConfiguration这个自动配置类因为不管这个类里写了什么代码Spring不去解析它那都是没用的此时我们需要SpringBoot在run方法中能找到WebServiceAutoConfiguration这个配置类并添加到Spring容器中。MyApplication是Spring的一个配置类但是MyApplication是我们传递给SpringBoot从而添加到Spring容器中去的而WebServiceAutoConfiguration就需要SpringBoot去自动发现而不需要程序员做任何配置才能把它添加到Spring容器中去而且要注意的是Spring容器扫描也是扫描不到WebServiceAutoConfiguration这个类的因为我们的扫描路径是com.seven.user而WebServiceAutoConfiguration所在的包路径为com.seven.springboot。那SpringBoot中是如何实现的呢通过SPI当然SpringBoot中自己实现了一套SPI机制也就是我们熟知的spring.factories文件那么我们模拟就不搞复杂了就直接用JDK自带的SPI机制。发现自动配置类为了实现这个功能以及为了最后的效果演示我们需要把springboot源码和业务代码源码拆分两个maven模块也就相当于两个项目最后的源码结构为现在我们只需要在springboot项目中的resources目录下添加如下目录META-INF/services和文件SPI的配置就完成了相当于通过com.seven.springboot.AutoConfiguration文件配置了springboot中所提供的配置类。并且提供一个接口javapublic interface AutoConfiguration { }并且WebServiceAutoConfiguration实现该接口javaConfiguration public class WebServerAutoConfiguration implements AutoConfiguration { Bean SevenConditionalOnClass(org.apache.catalina.startup.Tomcat) public TomcatWebServer tomcatWebServer(){ return new TomcatWebServer(); } Bean SevenConditionalOnClass(org.eclipse.jetty.server.Server) public JettyWebServer jettyWebServer(){ return new JettyWebServer(); } }然后我们再利用spring中的Import技术来导入这些配置类我们在SevenSpringBootApplication的定义上增加如下代码javaTarget(ElementType.TYPE) Retention(RetentionPolicy.RUNTIME) Configuration ComponentScan Import(SevenImportSeclet.class) public interface SevenSpringBootApplication { }SevenImportSeclet类为javapublic class SevenImportSeclet implements DeferredImportSelector { Override public String[] selectImports(AnnotationMetadata importingClassMetadata) { // 自动配置 ServiceLoaderAutoConfiguration loader ServiceLoader.load(AutoConfiguration.class); ListString list new ArrayList(); for (AutoConfiguration configuration : loader) { list.add(configuration.getClass().getName()); } return list.toArray(new String[0]); }