前言
想必写Java的都知道spring这个框架,使用其实也是很方便了,但是仍然需要大量的配置,为了进一步提升开发的效率,业内大佬就开发出了一个springboot的框架,只要进行一些简单的配置,就可以进入快速的开发,那么它的底层是怎么做到这个呢,其实仍然是基于spring进行开发的,只是自动帮我们配置了很多类,我们就不需要再自己进行配置了,这篇文章就是个人对于这个机制的一些简单理解
项目模块
模块介绍
其中zxcBoot是用来模拟springBoot启动原理的,而user则是我们平时的使用模块,主要涉及的不多,因为这是简单的模拟
auto: 自动导入配置类
condition: 条件判断bean是否要生成
config: 自动配置类
core: 核心的主键和启动类
server: 服务器启动相关
下面就一个个来分析这些包是干啥的
注:这篇文章只是在探究springboot的原理,所以对于spring的知识并不会深入讲解,也就是说如果不懂spring的机制有些地方可能会看不懂
springBoot核心机制
我们自己如果要实现一个springBoot,关注的问题主要有以下几个
1. 既然boot是基于spring的,那么就需要创建spring容器
2. 为了实现可以在浏览器进行访问,那么就必须启动Tomcat之类的容器
3. 由于要实现Tomcat,Jetty等容器的自动切换需要依赖于spring的条件注解
4. 配置类要统一生效需要依赖spring的DeferredImportSelector接口
5. 为了自动加载这些配置类利用了SPI机制
6. 最终为了让用户更容易使用,提供了最底层的注解
以下基于这些问题来进行说明
核心注解
这里先放着用户需要使用的一个注解,也是最核心的,如下,然后再展开一点点讲
package com.zxc.boot.core;
import com.zxc.boot.auto.ZxcImportAutoSelector;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Configuration标明为配置类
* ComponentScan扫描包路径,默认是当前包及子包
* @Import(ZxcImportAutoSelector.class) 导入了自动配置类所在的路径
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Configuration
@ComponentScan
@Import(ZxcImportAutoSelector.class)
public @interface ZxcSpringBoot {
}
spring容器创建和tomcat启动
package com.zxc.boot.core;
import com.zxc.boot.server.WebServer;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
public class ZxcApplication {
public static void run(Class clazz) {
//创建spring容器
AnnotationConfigWebApplicationContext webApplicationContext = new AnnotationConfigWebApplicationContext();
//注册clazz
webApplicationContext.register(clazz);
//刷新容器
webApplicationContext.refresh();
//从容器获取一个WebServer并启动,这是核心的扩展点位置..
//从容器获取,也是实现自动化切换实现的关键所在,,,
WebServer webServer = getWebServer(webApplicationContext);
webServer.start();
}
public static WebServer getWebServer(AnnotationConfigWebApplicationContext webApplicationContext) {
//这个方法不会初始化数据,有一定提升性能帮助
String[] beanNamesForType = webApplicationContext.getBeanNamesForType(WebServer.class);
if(beanNamesForType.length <= 0) {
throw new RuntimeException("没有WebServer实现类,请检查");
}
if(beanNamesForType.length > 1) {
throw new RuntimeException("存在多个WebServer实现类,请检查");
}
return webApplicationContext.getBean(WebServer.class);
}
}
这部分没什么好说的,就是创建一个容器,然后把clazz放到容器中,通常这个clazz都是spring的一个配置类,然后刷新容器,此时spring容器会创建所有单例并且非懒加载的bean,接着是从容器中获取一个WebServer接口(TomcatServer实现的接口),然后启动
这里的精华在于是从Spring容器中获取对象的,为后面的自动切换会用户的替换提供了基础,还有个细节是在不需要获取到bean实施的时候可以调用getBeanNamesForType方法,该方法只是获取bean定义,并不会生成bean,这也算是一个优化了,有了这些基础的东西,就可以慢慢的实现springboot自动装配原理了
WebServer自动装配类
package com.zxc.boot.config;
import com.zxc.boot.core.ZxcAutoConfig;
import com.zxc.boot.server.JettyServer;
import com.zxc.boot.server.TomcatWebServer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class WebServerAutoConfig implements ZxcAutoConfig {
@Bean
public TomcatWebServer tomcatWebServer() {
return new TomcatWebServer();
}
@Bean
public JettyServer jettyServer() {
return new JettyServer();
}
}
看起来很简单,只是往spring容器放置了一些bean,两个都是WebServer接口的实现类,熟练spring的人一看就能发现其实这是有问题的,因为上面从容器中只是获取一个WebServer的实现类,而现在容器中有两个,肯定是会抛出异常的,所以这样肯定是不行的,为了解决这个问题,就需要利用spring提供的条件注解了
条件注解
package com.zxc.boot.condition;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
import java.util.Map;
public class MyConditionOnClassImpl implements Condition {
@Override
public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
Map<String, Object> attributes = annotatedTypeMetadata.getAnnotationAttributes(MyConditionOnClass.class.getName());
Object className = attributes.get("className");
try {
conditionContext.getClassLoader().loadClass(className.toString());
return true;
} catch (Exception e) {
return false;
}
}
}
package com.zxc.boot.condition;
import org.springframework.context.annotation.Conditional;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Configuration标明为配置类
* ComponentScan扫描包路径,默认是当前包及子包
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Conditional(MyConditionOnClassImpl.class)
public @interface MyConditionOnClass {
String className();
}
其中MyConditionOnClassImpl是实现了spring的条件接口Condition,matches返回true的时候spring就会创建该bean,否则就不会创建,而MyConditionOnClass注解只是为了更方便的使用而定义出来的,也就是说以下两种使用方式是一样的,意思都是让spring校验条件成功时才返回,但是有了一层注解的保证,使用起来用户更方便,也更加能看出具体意思
@Bean
@MyConditionOnClass(className = "org.apache.catalina.startup.Tomcat")
public TomcatWebServer tomcatWebServer() {
return new TomcatWebServer();
}
@Bean
@Conditional(MyConditionOnClassImpl.class)
public TomcatWebServer tomcatWebServer2() {
return new TomcatWebServer();
}
有了这个条件注解后,之前的自动配置类就可以改为如下了
package com.zxc.boot.config;
import com.zxc.boot.condition.MyConditionOnClass;
import com.zxc.boot.core.ZxcAutoConfig;
import com.zxc.boot.server.JettyServer;
import com.zxc.boot.server.TomcatWebServer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class WebServerAutoConfig implements ZxcAutoConfig {
@Bean
@MyConditionOnClass(className = "org.apache.catalina.startup.Tomcat")
public TomcatWebServer tomcatWebServer() {
return new TomcatWebServer();
}
@Bean
@MyConditionOnClass(className = "jetty的某个类")
public JettyServer jettyServer() {
return new JettyServer();
}
}
这样就不会有多个WebServer的问题了,除非你引入了多个依赖,这个也是实现自动装配的核心原理,因为MyConditionOnClassImpl的matches方法其实就是对你配置的全类名进行加载,如果加载到类就返回匹配,spring就会去创建,如果加载不到类那就不会返回了,所以你就可以通过引入对应的依赖来解决自动切换的问题,不过有了这个自动配置类以后你还是要交给spring管理,怎么弄呢,就需要以下的配置了
自动配置类加载
首先,其中一种做法就是直接使用@Import注解,对核心的注解改造,如下
package com.zxc.boot.core;
import com.zxc.boot.config.WebServerAutoConfig;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Configuration标明为配置类
* ComponentScan扫描包路径,默认是当前包及子包
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Configuration
@ComponentScan
@Import(WebServerAutoConfig.class)
public @interface ZxcSpringBoot {
}
这样就可以了,但是这样会有个问题,如果你有大量的自动配置类需要加入到spring容器中,那么就需要写很多这样的导入,显然是很不方便的,所以下面就需要利用spring提供的另外机制,来解决这个问题
ImportSelector机制
ImportSelector机制也是spring提供的,用于批量导入bean的接口方法,它的方法定义如下,意思就是说String[]数组是类的全类名,只要你返回,spring就会进行去解析
public interface ImportSelector {
/**
* Select and return the names of which class(es) should be imported based on
* the {@link AnnotationMetadata} of the importing @{@link Configuration} class.
*/
String[] selectImports(AnnotationMetadata importingClassMetadata);
}
基于这种机制,我们就可以提供这么个实现类,如下,把要返回的全类名直接返回即可
package com.zxc.boot.auto;
import com.zxc.boot.config.WebServerAutoConfig;
import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.type.AnnotationMetadata;
/**
* 返回要放到容器中的全类名
*/
public class ZxcImportAutoSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
return new String[]{WebServerAutoConfig.class.getName()};
}
}
然后注解改为这样即可
package com.zxc.boot.core;
import com.zxc.boot.auto.ZxcImportAutoSelector;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Configuration标明为配置类
* ComponentScan扫描包路径,默认是当前包及子包
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Configuration
@ComponentScan
@Import(ZxcImportAutoSelector.class)
public @interface ZxcSpringBoot {
}
这样一来,如果有多个自动配置类就可以直接在ZxcImportAutoSelector声明即可了,但是这样还是存在一个问题,如果使用者想进行扩展是很麻烦的,因为有了SPI机制来解决这个问题,下面来看看这个方案是怎么样的
Java SPI机制解决动态化
Java SPI机制大概就是说可以通过一个配置文件来自动加载所有的实现类,首先,需要提供一个接口,比如这里的
package com.zxc.boot.core;
/**
* 空接口,用于SPI机制
*/
public interface ZxcAutoConfig {
}
其次让我们的WebServerAutoConfig去实现这个接口
然后在resources资源下建立 META-INF/services 文件夹,注意,是两个文件夹,先创建META-INF文件夹,然后在META-INF文件夹下面创建一个services文件夹,接着在services创建一个文件,文件名为接口的全类名:com.zxc.boot.core.ZxcAutoConfig,注意,文件没有后缀名的,然后把具体的实现放到文件中即可,如果有多个换行就行了,如下
这些操作做完以后我们就可以使用对之前的进行改造了,利用 ServiceLoader.load()方法来加载,方法是java spi提供的,它会到指定的文件夹下去加载实现类,也就是META-INF/services,注意这个路径是代码写死的,只能按照这种规范来放文件,如下
package com.zxc.boot.auto;
import com.zxc.boot.core.ZxcAutoConfig;
import org.springframework.context.annotation.DeferredImportSelector;
import org.springframework.core.type.AnnotationMetadata;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.ServiceLoader;
/**
* 返回要放到容器中的全类名
*/
public class ZxcImportAutoSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
List<String> configList = new ArrayList<>();
Iterator<ZxcAutoConfig> iterator = ServiceLoader.load(ZxcAutoConfig.class).iterator();
while (iterator.hasNext()) {
configList.add(iterator.next().getClass().getName());
}
return configList.toArray(new String[0]);
}
}
这样一来,我们就不需要在代码中写死配置类全路径了,而且如果用户需要扩展的化它也可
以根据spi的机制,在META-INF/service建立对应的文件,然后提供自己的配置类即可,因为ServiceLoader.load()是会加载所有的文件的,到此,boot的核心机制就写完了,这里还有个问题顺便体现,ImportSelector接口是跟其他bean一起初始化的,假设有这么个情况,用户自己也定义了一个WebServerAutoConfig类,那么有可能会当前我们定义的覆盖掉,也就是存在一个顺序问题,对于这种情况,spring提供了一个子接口,叫DeferredImportSelector,这个接口是ImportSelector的子接口,它的功能是会先加载其他的bean,最后才来加载这个接口下面的bean,从而避免了顺序问题,当然了,如果要为了避免的话,就得在WebServerAutoConfig多添加一些条件判断了,如下
package com.zxc.boot.config;
import com.zxc.boot.condition.MyConditionOnClass;
import com.zxc.boot.core.ZxcAutoConfig;
import com.zxc.boot.server.JettyServer;
import com.zxc.boot.server.TomcatWebServer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConditionalOnMissingBean(WebServerAutoConfig.class)
public class WebServerAutoConfig implements ZxcAutoConfig {
@Bean
@MyConditionOnClass(className = "org.apache.catalina.startup.Tomcat")
public TomcatWebServer tomcatWebServer() {
return new TomcatWebServer();
}
@Bean
@MyConditionOnClass(className = "jetty的某个类")
public JettyServer jettyServer() {
return new JettyServer();
}
}
其中,@ConditionalOnMissingBean是springboot的注解,意思是说没有WebServerAutoConfig的bean我当前的bean才会生效,假设用户自动定义了我这个就不会生效了,当然了这里只是演示下,没有真正的引入,所以我们最后的importSelect就是如下
package com.zxc.boot.auto;
import com.zxc.boot.core.ZxcAutoConfig;
import org.springframework.context.annotation.DeferredImportSelector;
import org.springframework.core.type.AnnotationMetadata;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.ServiceLoader;
/**
* 返回要放到容器中的全类名
*/
public class ZxcImportAutoSelector implements DeferredImportSelector {
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
List<String> configList = new ArrayList<>();
Iterator<ZxcAutoConfig> iterator = ServiceLoader.load(ZxcAutoConfig.class).iterator();
while (iterator.hasNext()) {
configList.add(iterator.next().getClass().getName());
}
return configList.toArray(new String[0]);
}
}
到这里,我们自己实现的简易boot就完成了,稍微总结一下,大概有这么几个核心点
1. 从spring容器中getWebServer,为后面的自动装配做好准备
2. 使用spring的condition条件功能实现不同实现类根据依赖不同而自动切换
3. 使用ImportSelector结合@Import注解来动态注入多个自动装配类
4. 使用spi机制来让其他使用者也可以很方便的进行扩展
当然了,上面还涉及很多spring的扩展点,这里主要是讲boot的核心原理,就不细说了,下面我们就来看一下使用,有了上面的支持,使用就非常简单了,跟我们平常依赖springboot的用法是差不多的
使用我们自己的springboot
首先要引入我们自己的依赖
<dependencies>
<!-- 依赖自己开发的springboot -->
<dependency>
<groupId>org.example</groupId>
<artifactId>zxcBoot</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
配置主启动类
package com.zxc.user;
import com.zxc.boot.core.ZxcApplication;
import com.zxc.boot.core.ZxcSpringBoot;
@ZxcSpringBoot
public class UserApplication {
public static void main(String[] args) {
ZxcApplication.run(UserApplication.class);
}
}
然后声明bean并使用
如下
package com.zxc.user.service;
import org.springframework.stereotype.Service;
@Service
public class UserService {
public String test() {
return "ZxcTest";
}
}
package com.zxc.user.controller;
import com.zxc.user.service.UserService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
@RequestMapping("/user")
public class UserController {
@Resource
private UserService userService;
@GetMapping("/test")
public String test() {
return userService.test();
}
}
接着启动UserApplication的main方法,然后在你控制台输入
http://localhost:8081/user/test
可以看到如下内容
到此,就结束了
注:虽然这里还是引用了@Service的spring注解,不过我们是通过zxcBoot项目间接去依赖的,而不是直接依赖的
项目地址
链接:https://share.weiyun.com/umgAPdFn 密码:a6axfv
注:项目名为:zxcSpringBoot.zip
总结
springboot的核心流程并不复杂,个人觉得只要用心去体会,就能搞懂这个原理,springboot里面的一些技术思想很值得我们去借鉴,这里也建议大家如果看的懂源码的话可以多看看,对我们提升技术还是很有帮助的