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:
- Create a Spring container
- Create Tomcat object
- Generate a DispatcherServlet object and bind it to the Spring container created earlier
- Add DispatcherServlet to Tomcat
- 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:
- If there is a dependency on Tomcat in the project, start Tomcat
- If the project has Jetty dependencies, start Jetty
- If there is neither, an error will be reported
- 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:
- Create an AnnotationConfigWebApplicationContext container
- Parse the MyApplication class and then scan
- Obtain the WebServer type bean from the Spring container through the getWebServer method
- 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.