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

系列文章目录

手写SpringBoot(一)之简易版SpringBoot
手写SpringBoot(二)之动态切换Servlet容器
手写SpringBoot(三)之自动配置
手写SpringBoot(四)之bean动态加载
手写SpringBoot(五)之整合AOP

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

本节着重介绍@ConditionOnClass的由来

我们在切换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();
    }
}

如何实现动态加载?

  1. 定义MyConditionalOnClass注解
  2. 利用Spring的@Conditional注解标记
  3. 定义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机制,

  1. 封装@ConditionOnClass注解,并将@Conditional注解组合到该注解上面,@conditionOnClass的核心就是@Condition
  2. 通过定义value属性,来暴力传参,将tomcat或者jetty的核心类名传到Condition接口的matches方法下
  3. 通过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容器扫描到,自然不会加载到容器中。

解决办法

  1. 在UserApplication中使用@Import(WebServerAutoConfiguration.class)将WebServerAutoConfiguration 配置类加载到Spring的Configuration中。但是这样对于用户来说,不太美好。
  2. @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只需提供接口。


手写SpringBoot(一)之简易版SpringBoot
手写SpringBoot(二)之动态切换Servlet容器
手写SpringBoot(三)之自动配置
手写SpringBoot(四)之bean动态加载
手写SpringBoot(五)之整合AOP

  • 14
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值