实现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类就可以啦。