实现Tomcat和Jetty的切换
前言
上一篇文章我们聊到,SpringBoot中内置了web服务器,包括Tomcat、Jetty,并且实现了SpringBoot启动Tomcat的流程。
那么SpringBoot怎样自动切换成Jetty服务器呢?
接下来我们继续学习如何实现Tomcat和Jetty的自动切换。
定义WebServer接口并实现
package com.ber.springboot;
import org.springframework.web.context.WebApplicationContext;
/**
* @Author 鳄鱼儿
* @Description TODO
* @date 2023/8/19 19:44
* @Version 1.0
*/public interface WebServer {
void start(WebApplicationContext applicationContext);
}
将BerSpringApplication类中startTomcat写到TomcatWebServer实现类中。
package com.ber.springboot;
import org.apache.catalina.*;
import org.apache.catalina.connector.Connector;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.core.StandardEngine;
import org.apache.catalina.core.StandardHost;
import org.apache.catalina.startup.Tomcat;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
/**
* @Author 鳄鱼儿
* @Description TODO
* @date 2023/8/19 19:45
* @Version 1.0
*/
public class TomcatWebServer implements WebServer{
@Override
public void start(WebApplicationContext applicationContext) {
System.out.println("启动Tomcat");
Tomcat tomcat = new Tomcat();
Server server = tomcat.getServer();
Service service = server.findService("Tomcat");
Connector connector = new Connector();
connector.setPort(8023);
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();
}
}
}
JettyWebServer类同样实现WebServer接口,不过具体启动Jetty代码省略,不在本文探讨范围内。
package com.ber.springboot;
/**
* @Author 鳄鱼儿
* @Description TODO
* @date 2023/8/19 19:46
* @Version 1.0
*/
public class JettyWebServer implements WebServer{
@Override
public void start() {
System.out.println("启动Jetty");
}
}
修改BerSpringApplication类
package com.ber.springboot;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import java.util.Map;
/**
* @Author 鳄鱼儿
* @Description TODO
* @date 2023/8/19 14:08
* @Version 1.0
*/
public class BerSpringApplication {
public static void run(Class clazz) {
// 1. 创建Spring 容器
AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext();
applicationContext.register(clazz);
applicationContext.refresh();
// 2. 获取特定WebServer类型的Bean
WebServer webServer = getWebServer(applicationContext);
// 3. 调用start方法
webServer.start(applicationContext);
}
private static WebServer getWebServer(AnnotationConfigWebApplicationContext 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();
}
}
在run方法中,获取到特定的web服务器,并通过start方法进行 启动。
getWebServer方法实现判断web服务器,并处理特殊情况——没有web服务器或者出现多个web服务器。
条件注解
package com.ber.springboot;
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;
/**
* @Author 鳄鱼儿
* @Description TODO
* @date 2023/8/19 20:06
* @Version 1.0
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Conditional(BerOnClassConsition.class)
public @interface BerConditionalOnClass {
String value() default "";
}
具体步骤为:
- 拿到@BerConditionalOnClass中的value属性
- 类加载器进行加载,加载到了特定的类名,则符合条件;否则不符合条件
package com.ber.springboot;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
import java.util.Map;
/**
* @Author 鳄鱼儿
* @Description TODO
* @date 2023/8/19 20:08
* @Version 1.0
*/
public class BerOnClassConsition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
Map<String, Object> annotationAttributes =
metadata.getAnnotationAttributes(BerConditionalOnClass.class.getName());
// 1. 拿到@BerConditionalOnClass中的value属性
String className = (String) annotationAttributes.get("value");
// 2. 类加载器进行加载
try {
// 2.1 加载到了特定的类名,则符合条件 true context.getClassLoader().loadClass(className);
return true;
} catch (ClassNotFoundException e) {
// 2.2 加载不到,则不符合条件 false return false;
}
}
}
自动配置类
package com.ber.springboot;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @Author 鳄鱼儿
* @Description TODO
* @date 2023/8/19 20:34
* @Version 1.0
*/
@Configuration
public class WebServiceAutoConfiguration implements AutoConfiguration{
@Bean
@BerConditionalOnClass("org.apache.catalina.startup.Tomcat")
public TomcatWebServer tomcatWebServer() {
return new TomcatWebServer();
}
@Bean
@BerConditionalOnClass("org.eclipse.jetty.server.Server")
public JettyWebServer jettyWebServer() {
return new JettyWebServer();
}
}
自动配置类在Spring Boot应用程序中起着关键的作用,它们是实现自动化配置的核心组件。
这里定义满足各自条件的Bean,当org.apache.catalina.startup.Tomcat类存在时,TomcatWebServer的Bean才存在,另一个亦是如此。
当spring容器存在Bean时,就可以通过BerSpringApplication类getWebServer方法中的applicationContext.getBeansOfType(WebServer.class)获取到,并由此可以进行对web服务器是否存在的判断。
SPI机制发现WebServiceAutoConfiguration
刚刚我们定义了自动配置类,但运行user模块的Userapplication启动类时,发现是无法发现WebServiceAutoConfiguration配置类的。
这是因为我们传入了Userapplication作为配置类,扫描路径为Userapplication所在的包路径,是无法扫描到WebServiceAutoConfiguration类的。
在springboot中实现了类似SPI的思想,就是项目中的spring.factories文件,提供了一种可插拔的扩展机制,使开发人员能够轻松地定制应用程序的行为和功能,同时又能保持主应用程序的稳定性。
这里我们可以借助JDK的SPI机制实现发现WebServiceAutoConfiguration类。
在springboot模块中增加resources/META-INF/services/com.ber.springboot.AutoConfiguration文件,具体路径如图所示:
com.ber.springboot.WebServiceAutoConfiguration
增加AutoConfiguration接口类和实现类。
package com.ber.springboot;
/**
* @Author 鳄鱼儿
* @Description TODO
* @date 2023/8/19 21:08
* @Version 1.0
*/
public interface AutoConfiguration {
}
package com.ber.springboot;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @Author 鳄鱼儿
* @Description TODO
* @date 2023/8/19 20:34
* @Version 1.0
*/
@Configuration
public class WebServiceAutoConfiguration implements AutoConfiguration{
@Bean
@BerConditionalOnClass("org.apache.catalina.startup.Tomcat")
public TomcatWebServer tomcatWebServer() {
return new TomcatWebServer();
}
@Bean
@BerConditionalOnClass("org.eclipse.jetty.server.Server")
public JettyWebServer jettyWebServer() {
return new JettyWebServer();
}
}
并在注解类@BerSpringBootApplication上增加@Import(BerImportSelect.class)
注解,BerImportSelect类从com.ber.springboot.AutoConfiguration文件中获取类名,然后添加到spring容器。
package com.ber.springboot;
import org.springframework.context.annotation.DeferredImportSelector;
import org.springframework.core.type.AnnotationMetadata;
import java.util.ArrayList;
import java.util.List;
import java.util.ServiceLoader;
/**
* @Author 鳄鱼儿
* @Description
* @date 2023/8/19 21:15
* @Version 1.0
*/
public class BerImportSelect implements DeferredImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
/** 使用Java的ServiceLoader机制加载实现了AutoConfiguration接口的类
* AutoConfiguration是Spring Boot中用于自动配置的接口
* AutoConfiguration的实现类通常包含了一些配置信息,帮助应用程序在不需要显式配置的情况下自动完成一些功能
*/
ServiceLoader<AutoConfiguration> serviceLoader = ServiceLoader.load(AutoConfiguration.class);
List<String> list = new ArrayList<>();
for (AutoConfiguration autoConfiguration : serviceLoader) {
list.add(autoConfiguration.getClass().getName());
}
// 返回包含所有加载的AutoConfiguration实现类名的字符串数组
return list.toArray(new String[0]);
}
}
添加Jetty依赖
修改user模块的依赖如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>simulate-springboot</artifactId>
<groupId>org.example</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>user</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.example</groupId>
<artifactId>springboot</artifactId>
<version>1.0-SNAPSHOT</version>
<exclusions>
<exclusion>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
<version>9.4.43.v20210629</version>
</dependency>
</dependencies>
</project>
这里需要排除tomcat依赖,因为springboot中已经添加了tomcat的依赖。
不排除就会出来既有tomcat又有Jetty,就会出现IllegalStateException异常。
到此运行user模块的UserApplication类就可以啦。