手写SpringBoot(二)之动态切换Servlet容器

简介: 我们在切换serlvet容器的时候,会将SpringBoot默认的tomcat jar包给排除掉,换上我们需要的jar包,比如jetty。

我们在切换serlvet容器的时候,会将SpringBoot默认的tomcat jar包给排除掉,换上我们需要的jar包,比如jetty。如下图所示


<?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">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>cn.axj</groupId>
        <artifactId>spring-boot-base</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <artifactId>user-service</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>


    <dependencies>
        <dependency>
            <groupId>cn.axj</groupId>
            <artifactId>my-spring-boot</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>

实现思路:


  1. 定义一个WebServer顶层接口
  2. 将tomcat和jetty的实现类加载到容器中,并根据条件判断,动态加载tomcat或者jetty的实现类
  3. 在servlet容器启动前动态获取WebServer,并通过WebServer启动


定义webServer

package cn.axj.springboot.my.web.container;

public interface WebServer {
    void start(WebApplicationContext webApplicationContext);
}

实现WebServer

package cn.axj.springboot.my.web.container;


public class TomcatWebServer implements WebServer{
    @Override
    public void start(WebApplicationContext webApplicationContext) {

    }
}

package cn.axj.springboot.my.web.container;

public class JettyWebServer implements WebServer{
    @Override
    public void start(WebApplicationContext webApplicationContext) {

    }
}

定义WebServerAutoConfiguration

package cn.axj.springboot.my.config;

import cn.axj.springboot.my.annnotation.MyConditionalOnClass;
import cn.axj.springboot.my.web.container.JettyWebServer;
import cn.axj.springboot.my.web.container.TomcatWebServer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class WebServerAutoConfiguration {


    /**
     * 根据jar包是否有 org.apache.catalina.startup.Tomcat类来判断是否加载tomcatServer
     * @return
     */
    @Bean
    @MyConditionalOnClass("org.apache.catalina.startup.Tomcat")
    public TomcatWebServer tomcatWebServer() {
        return new TomcatWebServer();
    }


    /**
     * 根据jar包是否有 org.eclipse.jetty.server.Server类来判断是否加载jettyServer
     * @return
     */
    @Bean
    @MyConditionalOnClass("org.eclipse.jetty.server.Server")
    public JettyWebServer jettyWebServer(){
        return new JettyWebServer();
    }
}

如何实现动态加载?


定义MyConditionalOnClass注解

利用Spring的@Conditional注解标记

定义Conditional条件判断类

定义MyConditionalOnClass注解,利用@Conditional注解定义动态加载逻辑


@Conditional源码如下,内部有一个Class对象需要实现Condition接口


public @interface Conditional {
    Class<? extends Condition>[] value();
}

@Conditional(MyClassCondition.class) 逻辑是通过Condition接口里面的matches方法动态判断

package cn.axj.springboot.my.annnotation;

import cn.axj.springboot.my.condition.MyClassCondition;
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;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE,ElementType.METHOD})
@Conditional(MyClassCondition.class)
public @interface MyConditionalOnClass {
    String value();
}

MyClassConditional如下

package cn.axj.springboot.my.condition;

import cn.axj.springboot.my.annnotation.MyConditionalOnClass;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;

import java.util.Map;
import java.util.Objects;

/**
 * 定义一个自定义的条件类
 * 该类主要用于根据条件动态加载Bean
 **/
public class MyClassCondition implements Condition {

    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        Map<String, Object> annotationAttributes = metadata.getAnnotationAttributes(MyConditionalOnClass.class.getName());

        /**
         * 获取{@link MyConditionalOnClass}注解中的属性值
         * 例如:@MyConditionalOnClass(value = "com.example.MyBean")
         * 则可以通过annotationAttributes.get("value")获取到"com.example.MyBean"
         */
        String className = (String) annotationAttributes.get("value");

        try {
            Objects.requireNonNull(context.getClassLoader()).loadClass(className);
        } catch (ClassNotFoundException e) {
            //没有找到该类,则返回false
            return false;
        }
        return true;
    }
}

总体实现逻辑,由Spring提供的@Conditional条件注解动态加载bean机制,


封装@ConditionOnClass注解,并将@Conditional注解组合到该注解上面,@conditionOnClass的核心就是@Condition

通过定义value属性,来暴力传参,将tomcat或者jetty的核心类名传到Condition接口的matches方法下

通过Condition的matches方法匹配是否加载该bean

至此已实现在Spring中动态加载WebServer,在MyApplication.run方法中,从Spring容器中获取WebServer对象,并开启WebServer


public static void run(Class<?> clazz,String[] args) {
        //启动Spring容器
        AnnotationConfigWebApplicationContext annotationConfigApplicationContext = new AnnotationConfigWebApplicationContext();
        annotationConfigApplicationContext.register(clazz);
        annotationConfigApplicationContext.refresh();
        //启动tomcat容器
        WebServer webServer = getWebServer(annotationConfigApplicationContext);
        webServer.start();
    }


 
   private static WebServer getWebServer(AnnotationConfigWebApplicationContext annotationConfigApplicationContext) {
        Map<String, WebServer> webServerMap = annotationConfigApplicationContext.getBeansOfType(WebServer.class);
        if(webServerMap.isEmpty()){
            throw new RuntimeException("web server is null");
        }
        if(webServerMap.size() > 1){
            throw new RuntimeException("找到多个web server,只能有一个WebServer" + webServerMap.values());
        }
        return webServerMap.values().stream().findFirst().get();
    }

至此,项目结构如下图



WebContainer已废弃


启动user-service模块,抛出异常


Exception in thread "main" org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'cn.axj.springboot.my.web.container.WebServer' available


由于Spring容器中不存在WebServer对象,这是为什么?


在 WebServerAutoConfiguration 中定义的WebServer两个对象不会被Spring扫描到,因为在@MySpringBootApplication中配置的@ComponentScan扫描的包路径并不包括my-spring-boot中的路径。所以不会被Spring容器扫描到,自然不会加载到容器中。


解决办法,


在UserApplication中使用@Import(WebServerAutoConfiguration.class)将WebServerAutoConfiguration 配置类加载到Spring的Configuration中。但是这样对于用户来说,不太美好。

将@Import(WebServerAutoConfiguration.class)加载到@MySpringbootApplication注解上,这样Spring在扫描该组合注解的时候,会扫描到Import标签,并将WebServerAutoConfiguration配置类解析并加载到容器中。

最后,实现TomcatWebServer和JettyWebServer的start()方法


tomcat


package cn.axj.springboot.my.web.container;


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;

public class TomcatWebServer implements WebServer{
    @Override
    public void start(WebApplicationContext webApplicationContext) {
        System.out.println("启动TomcatWeb容器");
        Tomcat tomcat = new Tomcat();

        Server server = tomcat.getServer();
        Service service = server.findService("Tomcat");

        Connector connector = new Connector();
        connector.setPort(8080);

        StandardEngine 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,Springmvc专属
        tomcat.addServlet(contextPath,"dispatcher",new DispatcherServlet(webApplicationContext));
        context.addServletMappingDecoded("/*","dispatcher");


        try {
            tomcat.start();
        } catch (LifecycleException e) {
            throw new RuntimeException(e);
        }
    }
}

jetty


这里先留个坑,这里实现应该不是由SpringBoot去实现。想一想,SpringBoot不可能将所有serlvet容器的jar包都引入,如果不引入,没有这个jar包如何实现?这里应该是由各servlet去适配。所以SpringBoot只需提供接口。

目录
相关文章
|
1月前
|
缓存 Java Spring
servlet和SpringBoot两种方式分别获取Cookie和Session方式比较(带源码) —— 图文并茂 两种方式获取Header
文章比较了在Servlet和Spring Boot中获取Cookie、Session和Header的方法,并提供了相应的代码实例,展示了两种方式在实际应用中的异同。
149 3
servlet和SpringBoot两种方式分别获取Cookie和Session方式比较(带源码) —— 图文并茂 两种方式获取Header
|
4月前
|
前端开发 容器
css 动态文本对齐自适应 — 文本宽度小于容器宽度时居中对齐,文本宽度大于容器宽度时居左对齐
css 动态文本对齐自适应 — 文本宽度小于容器宽度时居中对齐,文本宽度大于容器宽度时居左对齐
39 2
|
4月前
|
前端开发 Java 应用服务中间件
Spring Boot 2.x 嵌入式 Servlet 容器
Spring Boot使用内嵌Tomcat,默认端口8080,可通过`application.properties`配置端口、上下文路径等。配置方式有两种:1) 直接在配置文件中添加`server.port`和`server.servlet.context-path`;2) 创建`WebServerFactoryCustomizer` Bean来自定义配置,如设置端口`factory.setPort(8083)`,这种方式优先级更高。
|
4月前
|
XML Java 应用服务中间件
springboot定制嵌入式的servlet
SpringBoot允许定制嵌入式Servlet容器,如修改配置或更换默认的Tomcat。配置可通过`application.properties`设置`server.port`和`server.tomcat.*`属性。此外,可创建`EmbeddedServletContainerCustomizer` Bean来自定义容器,例如改变端口。要替换默认的Tomcat,需排除`spring-boot-starter-tomcat`依赖,并引入`spring-boot-starter-jetty`。
|
4月前
|
Java Spring 容器
深入理解Spring Boot中的容器与依赖注入
深入理解Spring Boot中的容器与依赖注入
|
5月前
|
Kubernetes 安全 测试技术
超大规模商用 K8s 场景下,阿里巴巴如何动态解决容器资源的按需分配问题?
超大规模商用 K8s 场景下,阿里巴巴如何动态解决容器资源的按需分配问题?
|
5月前
|
Java API Docker
使用Spring Boot和Docker进行容器化部署
使用Spring Boot和Docker进行容器化部署
|
5月前
|
JavaScript Java 测试技术
基于SpringBoot+Vue的宝鸡文理学院学生成绩动态追踪系统的详细设计和实现(源码+lw+部署文档+讲解等)
基于SpringBoot+Vue的宝鸡文理学院学生成绩动态追踪系统的详细设计和实现(源码+lw+部署文档+讲解等)
29 0
|
5月前
|
Java 应用服务中间件 Maven
SpringBoot(六)之内嵌容器
需要用哪个容器,就把其他两个容器注释掉。(
48 0
|
5天前
|
Kubernetes Cloud Native Docker
云原生时代的容器化实践:Docker和Kubernetes入门
【10月更文挑战第37天】在数字化转型的浪潮中,云原生技术成为企业提升敏捷性和效率的关键。本篇文章将引导读者了解如何利用Docker进行容器化打包及部署,以及Kubernetes集群管理的基础操作,帮助初学者快速入门云原生的世界。通过实际案例分析,我们将深入探讨这些技术在现代IT架构中的应用与影响。
23 2