新建一个工程,包含两个 module:
springboot 模块,表示 springboot 源码实现;
user 模块,表示业务系统,使用 springboot 模块;
依赖包:Spring、SpringMVC、Tomcat 等,引入依赖如下:
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.18</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>5.3.18</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.3.18</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>9.0.60</version>
</dependency>
</dependencies>
在 user 模块下引入依赖:
<dependencies>
<dependency>
<groupId>org.example</groupId>
<artifactId>springboot</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
定义对应的 controller 和 service:
@RestController
public class UserController {
@Autowired
private UserService userService;
@GetMapping("test")
public String test(){
return userService.test();
}
}
最终希望通过启动 MyApplication 的 main 方法,启动项目,能访问到 UserController。
SpringBoot 的核心类和注解:
@SpringBootApplication,这个注解是加在应用启动类上的,也就是 main 方法所在的类;
SpringApplication,这个类中有个 run() 方法,用来启动 SpringBoot 应用的;
所以,自定义类和注解以实现上面的功能。
@FireSpringBootApplication 注解:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Configuration
@ComponentScan
public @interface FireSpringBootApplication {
}
FireSpringApplication 启动类:
public class FireSpringApplication {
public static void run(Class clazz){
}
}
在 MyApplication 中使用:
@FireSpringBootApplication
public class MyApplication {
public static void main(String[] args) {
FireSpringApplication.run(MyApplication.class);
}
}
需要在 run 方法中启动 tomcat,通过 tomcat 接收请求;
DispatchServlet 绑定 spring 容器,DispatchServlet 接收到请求后需要在 spring 容器中找到一个 controller 中对应的方法;
run 方法中需要实现的逻辑:
public class FireSpringApplication {
public static void run(Class clazz){
AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext();
applicationContext.register(clazz);
applicationContext.refresh();
}
}
run 方法中传入的即使 MyApplication 类,被解析为 Spring 容器的配置类;
默认会将 MyApplication 所在的包作为扫描路径,从而扫描到 UserController 和 UserService,所以在 spring 容器启动后就会存在两个 bean 了;
使用内嵌的 Tomact,即 Embed-Tomcat,启动代码如下:
public 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);
// 添加DispatcherServlet,并且绑定一个Spring容器
tomcat.addServlet(contextPath, "dispatcher", new DispatcherServlet(applicationContext));
// 设置Mapping关系
context.addServletMappingDecoded("/*", "dispatcher");
try {
tomcat.start();
} catch (LifecycleException e) {
e.printStackTrace();
}
}
在 run 方法中调用 startTomcat 方法启动 tomcat:
public static void run(Class clazz){
AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext();
applicationContext.register(clazz);
applicationContext.refresh();
// 启动tomcat
startTomcat(applicationContext);
}
到此,一个简单的 SpringBoot 就写出来了,运行 MyApplication 正常启动项目,通过浏览器就可以访问 UserController 了。
前面代码中默认启动的是 Tomcat,现在想改成这样子:
这个逻辑希望 SpringBoot 自动实现,对于程序员用户而言,只要在 Pom 文件中添加相关依赖就可以了,想用 Tomcat 就加 Tomcat 依赖,想用 Jetty 就加 Jetty 依赖。
Tomcat 和 Jetty 都是应用服务器,或者是 Servlet 容器,可以定义接口来表示它们,这个接口交 WebServer(SpringBoot 源码中也叫这个)。
定义接口如下:
public interface WebServer {
public void start();
}
Tomcat 实现类:
public class TomcatWebServer implements WebServer{
@Override
public void start() {
System.out.println("启动Tomcat");
}
}
Jetty 实现类:
public class JettyWebServer implements WebServer{
@Override
public void start() {
System.out.println("启动Jetty");
}
}
在 FireSpringApplication 中的 run 方法中,去获取对应的 WebServer,然后启动对应的 webServer。
代码如下:
public static void run(Class clazz){
AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext();
applicationContext.register(clazz);
applicationContext.refresh();
// 自动获取配置的Tomcat或者Jetty容器
WebServer webServer = getWebServer(applicationContext);
webServer.start();
}
public static WebServer getWebServer(ApplicationContext applicationContext){
return null;
}
首先实现一个条件注解@FireConditionalOnClass
,对应代码如下:
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Conditional(FireOnClassCondition.class)
public @interface FireConditionalOnClass {
String value() default "";
}
注意核心为@Conditional(FireOnClassCondition.class)
中的 FireOnClassCondition,因为它才是真正得条件逻辑:
public class FireOnClassCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
Map<String, Object> annotationAttributes =
metadata.getAnnotationAttributes(FireConditionalOnClass.class.getName());
String className = (String) annotationAttributes.get("value");
try {
context.getClassLoader().loadClass(className);
return true;
} catch (ClassNotFoundException e) {
return false;
}
}
}
具体逻辑为,拿到@FireConditionalOnClass
中的 value 属性,然后用类加载器进行加载,如果加载到了所指定的这个类,那就表示符合条件,如果加载不到,则表示不符合条件。
配置类代码如下:
@Configuration
public class WebServiceAutoConfiguration {
@Bean
@FireConditionalOnClass("org.apache.catalina.startup.Tomcat")
public TomcatWebServer tomcatWebServer(){
return new TomcatWebServer();
}
@Bean
@FireConditionalOnClass("org.eclipse.jetty.server.Server")
public JettyWebServer jettyWebServer(){
return new JettyWebServer();
}
}
表示org.apache.catalina.startup.Tomcat
存在,则有 tomcatWebServer 这个bean;
表示org.eclipse.jetty.server.Server
存在,则有 jettyWebServer 这个bean;
FireSpringApplication#getWebServer()方法实现:
public static WebServer getWebServer(ApplicationContext applicationContext){
// key为beanName, value为Bean对象
Map<String, WebServer> webServers = applicationContext.getBeansOfType(WebServer.class);
if (webServers.isEmpty()) {
throw new NullPointerException();
}
if (webServers.size() > 1) {
throw new IllegalStateException();
}
// 返回唯一的一个
return webServers.values().stream().findFirst().get();
}
这样整体 SpringBoot 启动逻辑就是这样的:
WebServiceAutoConfiguration 需要被 SpringBoot 发现,可以通过 SPI 机制实现,比较 JDK 自带的 SPI 来实现。
在 springboot 项目中的 resources 目录下添加目录META-INF/services
和文件 org.example.springboot.AutoConfiguration
,文件内容为org.example.springboot.WebServiceAutoConfiguration
。
接口:
public interface AutoConfiguration {
}
WebServiceAutoConfiguration 实现该接口:
@Configuration
public class WebServiceAutoConfiguration implements AutoConfiguration {
@Bean
@FireConditionalOnClass("org.apache.catalina.startup.Tomcat")
public TomcatWebServer tomcatWebServer(){
return new TomcatWebServer();
}
@Bean
@FireConditionalOnClass("org.eclipse.jetty.server.Server")
public JettyWebServer jettyWebServer(){
return new JettyWebServer();
}
}
再利用 spring 中的@Import
技术来导入这些配置类,我们在@FireSpringBootApplication
的定义上增加如下代码:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Configuration
@ComponentScan
@Import(FireImportSelect.class)
public @interface FireSpringBootApplication {
}
FireImportSelect:
public class FireImportSelect implements DeferredImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
ServiceLoader<AutoConfiguration> serviceLoader = ServiceLoader.load(AutoConfiguration.class);
List<String> list = new ArrayList<>();
for (AutoConfiguration autoConfiguration : serviceLoader) {
list.add(autoConfiguration.getClass().getName());
}
return list.toArray(new String[0]);
}
}
如此,Spring 容器可以装载 WebServiceAutoConfiguration 配置类了,对于 user 模块而言,不需要修改代码就可以自动识别 Tomcat 和 Jetty 了。
到此,实现了一个简单版本的 SpringBoot,因为 SpringBoot 首先是基于 Spring 的,而且提供的功能也更加强大,后面会对这些功能进行更深入的剖析。