[SpringBoot] Handwriting simulates the core process of SpringBoot

Dependency package

Create a new project containing two modules:

springboot module, representing springboot source code implementation;
The user module represents the business system and uses the springboot module;

Dependency packages: Spring, SpringMVC, Tomcat, etc., introduce dependencies as follows:

<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>

Introduce dependencies under the user module:

<dependencies>
    <dependency>
        <groupId>org.example</groupId>
        <artifactId>springboot</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
</dependencies>

Define the corresponding controller and service:

@RestController
public class UserController {<!-- -->

    @Autowired
    private UserService userService;

    @GetMapping("test")
    public String test(){<!-- -->
        return userService.test();
    }
}

Ultimately, we hope to access the UserController by starting the main method of MyApplication and starting the project.

Core annotations and core classes

SpringBoot’s core classes and annotations:

@SpringBootApplication, this annotation is added to the application startup class, which is the class where the main method is located;
SpringApplication, there is a run() method in this class, which is used to start the SpringBoot application;

Therefore, customize classes and annotations to achieve the above functions.
@FireSpringBootApplication annotation:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Configuration
@ComponentScan
public @interface FireSpringBootApplication {<!-- -->
}

FireSpringApplication startup class:

public class FireSpringApplication {<!-- -->
    public static void run(Class clazz){<!-- -->
    }
}

Use in MyApplication:

@FireSpringBootApplication
public class MyApplication {<!-- -->

    public static void main(String[] args) {<!-- -->
        FireSpringApplication.run(MyApplication.class);
    }
}

run method

You need to start tomcat in the run method and receive requests through tomcat;
DispatchServlet is bound to the spring container. After receiving the request, DispatchServlet needs to find a corresponding method in the controller in the spring container;

The logic that needs to be implemented in the run method:

  1. Create a Spring container
  2. Create Tomcat object
  3. Generate a DispatcherServlet object and bind it to the Spring container created earlier
  4. Add DispatcherServlet to Tomcat
  5. Start Tomcat

Create Spring container

public class FireSpringApplication {<!-- -->

    public static void run(Class clazz){<!-- -->
        AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext();
        applicationContext.register(clazz);
        applicationContext.refresh();
        
    }
}

The MyApplication class passed in the run method is parsed into the configuration class of the Spring container;
By default, the package where MyApplication is located will be used as the scanning path, thereby scanning UserController and UserService, so there will be two beans after the spring container is started;

Start Tomcat

Using embedded Tomact, namely Embed-Tomcat, the startup code is as follows:

public static void startTomcat(WebApplicationContext applicationContext){<!-- -->
    
    Tomcat tomcat = new Tomcat();
    
    Server server = tomcat.getServer();
    Service service = server.findService("Tomcat");
    
    Connector connector = new Connector();
//Bind port
    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);
    //Add DispatcherServlet and bind a Spring container
    tomcat.addServlet(contextPath, "dispatcher", new DispatcherServlet(applicationContext));
//Set Mapping relationship
    context.addServletMappingDecoded("/*", "dispatcher");
    
    try {<!-- -->
        tomcat.start();
    } catch (LifecycleException e) {<!-- -->
        e.printStackTrace();
    }
    
}

Call the startTomcat method in the run method to start tomcat:

public static void run(Class clazz){<!-- -->
    AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext();
    applicationContext.register(clazz);
    applicationContext.refresh();
    //Start tomcat
    startTomcat(applicationContext);
    
}

At this point, a simple SpringBoot has been written. Run MyApplication to start the project normally, and the UserController can be accessed through the browser.

Realize switching between Tomcat and Jetty

In the previous code, Tomcat is started by default. Now I want to change it to this:

  1. If there is a dependency on Tomcat in the project, start Tomcat
  2. If the project has Jetty dependencies, start Jetty
  3. If there is neither, an error will be reported
  4. If there are both, an error will be reported

This logic is expected to be automatically implemented by SpringBoot. For programmer users, just add relevant dependencies in the Pom file. If you want to use Tomcat, add Tomcat dependencies, and if you want to use Jetty, add Jetty dependencies.
Tomcat and Jetty are both application servers, or Servlet containers, and interfaces can be defined to represent them. This interface is passed to WebServer (also called this in the SpringBoot source code).
The interface is defined as follows:

public interface WebServer {<!-- -->
    
    public void start();
    
}

Tomcat implementation class:

public class TomcatWebServer implements WebServer{<!-- -->

    @Override
    public void start() {<!-- -->
        System.out.println("Start Tomcat");
    }
}

Jetty implementation class:

public class JettyWebServer implements WebServer{<!-- -->

    @Override
    public void start() {<!-- -->
       System.out.println("Start Jetty");
    }
}

In the run method in FireSpringApplication, obtain the corresponding WebServer, and then start the corresponding webServer.
code show as below:

public static void run(Class clazz){<!-- -->
    AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext();
    applicationContext.register(clazz);
    applicationContext.refresh();
    // Automatically obtain the configured Tomcat or Jetty container
    WebServer webServer = getWebServer(applicationContext);
    webServer.start();
    
}

public static WebServer getWebServer(ApplicationContext applicationContext){<!-- -->
    return null;
}

Annotations on simulation implementation conditions

First implement a conditional annotation @FireConditionalOnClass, the corresponding code is as follows:

@Target({<!-- --> ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Conditional(FireOnClassCondition.class)
public @interface FireConditionalOnClass {<!-- -->
    String value() default "";
}

Note that the core is FireOnClassCondition in @Conditional(FireOnClassCondition.class), because it is the real conditional logic:

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;
        }
    }
}

The specific logic is to get the value attribute in @FireConditionalOnClass, and then use the class loader to load it. If the specified class is loaded, it means that it meets the conditions. If it cannot be loaded, it means Ineligible.

Simulating automatic configuration classes

The configuration class code is as follows:

@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();
    }
}

Indicates that org.apache.catalina.startup.Tomcat exists, then there is the tomcatWebServer bean;
Indicates that org.eclipse.jetty.server.Server exists, and there is a jettyWebServer bean;

FireSpringApplication#getWebServer() method implementation:

public static WebServer getWebServer(ApplicationContext applicationContext){<!-- -->
    // key is beanName, value is Bean object
    Map<String, WebServer> webServers = applicationContext.getBeansOfType(WebServer.class);
    
    if (webServers.isEmpty()) {<!-- -->
        throw new NullPointerException();
    }
    if (webServers.size() > 1) {<!-- -->
        throw new IllegalStateException();
    }
    
    // Return the only one
    return webServers.values().stream().findFirst().get();
}

In this way, the overall SpringBoot startup logic is like this:

  1. Create an AnnotationConfigWebApplicationContext container
  2. Parse the MyApplication class and then scan
  3. Obtain the WebServer type bean from the Spring container through the getWebServer method
  4. Call the start method of the WebServer object

Discover automatic configuration classes

WebServiceAutoConfiguration needs to be discovered by SpringBoot and can be implemented through the SPI mechanism, compared with the SPI that comes with the JDK.

Add the directory META-INF/services and the file org.example.springboot.AutoConfiguration to the resources directory in the springboot project. The file content is org.example.springboot .WebServiceAutoConfiguration.

interface:

public interface AutoConfiguration {<!-- -->
}

WebServiceAutoConfiguration implements this interface:

@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();
    }
}

Then use the @Import technology in spring to import these configuration classes. We add the following code to the definition of @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]);
    }
}

In this way, the Spring container can load the WebServiceAutoConfiguration configuration class. For the user module, Tomcat and Jetty can be automatically recognized without modifying the code.

Summary

At this point, a simple version of SpringBoot has been implemented, because SpringBoot is first based on Spring and provides more powerful functions. These functions will be analyzed in more depth later.