0 EasySpring-Boot (一)
项目简介:
EasySpring-Boot是一个简易版的Spring Boot框架的复现,旨在帮助开发者更好地理解Spring Boot框架的核心原理和功能。通过实现基本的依赖注入、自动配置和Web功能,EasySpring-Boot展示了一个简单的应用程序框架的搭建过程。
在EasySpring-Boot中,实现了一个简单的应用上下文(MyApplicationContext)来管理Bean的注册和依赖注入,以及一个简单的配置类(MyConfiguration)来定义Bean和自动配置。通过这个简易版的Spring Boot框架,开发者可以更深入地了解Spring框架的工作原理,并在此基础上进行扩展和定制化开发。
EasySpring-Boot项目旨在帮助初学者和开发者更好地理解Spring Boot框架的实现原理,同时也可以作为学习和实践Spring框架的一个简单示例项目。源代码会在后续发出
1. 项目准备
1.1 原始项目依赖引入
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.23</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>5.3.23</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.3.23</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.3.23</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>9.0.65</version>
</dependency>
1.1.1 依赖解析
- org.springframework:spring-context:5.3.23:Spring框架的核心容器,提供IoC(控制反转)和DI(依赖注入)功能,包括BeanFactory、ApplicationContext等。
- org.springframework:spring-web:5.3.23:Spring Web模块,提供构建Web应用程序的基本功能和特性,如Web框架、RESTful服务等。
- org.springframework:spring-webmvc:5.3.23:Spring MVC模块,提供基于MVC(模型-视图-控制器)架构的Web应用程序开发支持,用于构建Web应用程序的控制器和视图。
- org.springframework:spring-aop:5.3.23:Spring AOP模块,提供面向切面编程的支持,用于实现横切关注点的模块化,如事务管理、日志记录等。
- javax.servlet:javax.servlet-api:3.1.0:Java Servlet API,用于支持开发基于Servlet的Web应用程序,定义了Servlet容器和Servlet规范。
- org.apache.tomcat.embed:tomcat-embed-core:9.0.65:Tomcat嵌入式核心,用于在应用程序中嵌入Tomcat容器,方便开发和测试Web应用程序。
1.1.2 依赖实现
当使用这些依赖时,可以通过以下方式来展示每个依赖的具体用途:
- org.springframework:spring-context:5.3.23:
- 例子:在Spring应用程序中创建并管理Bean
- 代码示例:
@Configuration
public class AppConfig {
@Bean
public MyBean myBean() {
return new MyBean();
}
}
- org.springframework:spring-web:5.3.23:
- 例子:创建一个简单的Spring Web应用程序
- 代码示例:
@Controller
public class MyController {
@RequestMapping("/")
public String home() {
return "index";
}
}
- org.springframework:spring-webmvc:5.3.23:
- 例子:使用Spring MVC构建一个基于MVC架构的Web应用程序
- 代码示例:
@Controller
public class MyController {
@RequestMapping("/hello")
public String hello(Model model) {
model.addAttribute("message", "Hello, Spring MVC!");
return "hello";
}
}
- org.springframework:spring-aop:5.3.23:
- 例子:使用Spring AOP实现日志记录
- 代码示例:
@Aspect
@Component
public class LoggingAspect {
@Before("execution(* com.example.service.*.*(..))")
public void logBefore(JoinPoint joinPoint) {
System.out.println("Before method: " + joinPoint.getSignature().getName());
}
}
- javax.servlet:javax.servlet-api:3.1.0:
- 例子:创建一个Servlet处理HTTP请求
- 代码示例:
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
PrintWriter out = response.getWriter();
out.println("Hello, Servlet!");
}
}
- org.apache.tomcat.embed:tomcat-embed-core:9.0.65:
- 例子:在Spring Boot应用中嵌入Tomcat容器
- 代码示例:无需特定代码示例,Spring Boot会自动嵌入Tomcat容器并运行应用程序。
1.2 项目结构
这里user模块已经装配了我们模拟的EasySpringBoot依赖
<artifactId>user</artifactId>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.bruan</groupId>
<artifactId>EasySpringBoot</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
2. 手写基础SpringBoot
创建SpringBootApplication注解 启动注解
首先需要模仿SpringBoot创建SpringBootApplication,我们给他一个自己的注解名称,我称之为:BrBootApplication
创建注解
- @Target(ElementType.TYPE):这个注解表示BrBootApplication注解可以应用在类上。即,这个自定义注解可以用来标记类。
- @Retention(RetentionPolicy.RUNTIME):这个注解表示BrBootApplication注解在运行时保留,这意味着这个自定义注解可以在运行时通过反射获取到。
- @Documented:这个注解表示BrBootApplication注解应该被 javadoc工具记录。当使用 javadoc 工具生成文档时,会包含这个注解的信息。
- @Inherited:这个注解表示BrBootApplication注解可以被子类继承。如果一个类使用了BrBootApplication注解,而这个类的子类没有使用其他的类级别的注解,那么子类也会继承这个注解。
这段代码定义了一个自定义注解BrBootApplication,该注解可以应用在类上,在运行时保留,并且可以被 javadoc 工具记录。同时,如果一个类使用了BrBootApplication注解,它的子类也会继承这个注解。这样的自定义注解可以用来标记特定的类,并在需要时通过反射获取注解信息。
user类使用该注解
不管如何,虽然该启动注解还没完善,但是我们已经可以使用该注解了,如下图:
模拟SpringBootApplication主类
运行逻辑
真正的SpringBoot框架已经帮我们实现了一个类,在启动类中,我们其实就是调用该类的Run方法,如下:
这段代码是一个典型的Spring Boot 应用程序的入口类,每部分的作用如下:
- **@SpringBootApplication:**这是一个Spring Boot提供的注解,它整合了多个注解,包括@Configuration、@EnableAutoConfiguration和@ComponentScan。这个注解的作用是标识这个类是Spring Boot应用程序的入口点,并且会自动扫描当前包及其子包中的组件。
- **public class TliasApplication:**这是定义的一个Java类,类名为TliasApplication,这个类是整个应用程序的入口点。
- **public static void main(String[] args):**这是Java程序的入口方法,当应用程序启动时,会首先执行这个方法。args参数是命令行参数,可以在启动应用程序时传入。
- **SpringApplication.run(TliasApplication.class, args):**这是Spring Boot提供的静态方法,用于启动Spring Boot应用程序。它接受两个参数,第一个参数是应用程序的主类(即包含@SpringBootApplication注解的类),第二个参数是命令行参数。调用这个方法会启动Spring Boot应用程序,并自动配置应用程序所需的环境。
综上所述,这段代码定义了一个Spring Boot应用程序的入口类,通过@SpringBootApplication注解标识这是一个Spring Boot应用程序,并在main方法中调用SpringApplication.run方法启动应用程序。这个类的目的是启动整个应用程序的运行。
模拟运行逻辑
这里我们定义一个类BRSpringbootApplication用于模拟主类,实现各种方法
现在我们可以使用该方法了,尽管逻辑还没写
run方法作用
思考一下执行完run() 会出现什么结果呢?
SpringApplication.run(TliasApplication.class, args) 方法后,会触发Spring Boot应用程序的启动过程。具体来说,会发生以下几个重要的步骤:
- Spring Boot应用程序会启动内嵌的Tomcat服务器(默认情况下)或者其他内嵌的Servlet容器。
- Spring Boot会自动扫描并加载应用程序中的所有组件(被@ComponentScan注解标识的类)。
- Spring Boot会自动配置应用程序的环境,包括加载配置文件、处理依赖注入、启用AOP等。
- Spring Boot会根据类路径下的依赖项自动配置应用程序,例如自动配置数据库连接、Web MVC等。
- 最终,应用程序会成功启动并监听指定的端口,等待处理HTTP请求或其他事件。
SpringBootApplication启动Tomcat
模拟运行Tomcat
public class BrSpringbootApplication {
/**
* 模拟run方法
* @param clazz
*/
public static void run(Class clazz){
//启动Tomcat
startTomcat();
}
private static void startTomcat() {
//使用Java代码启动
Tomcat tomcat = new Tomcat();
Server server = tomcat.getServer();
Service service = server.findService("Tomcat");
Connector connector = new Connector();
connector.setPort(8081);
Engine engine = new StandardEngine();
engine.setDefaultHost("localhost");
StandardHost host = new StandardHost();
host.setName("localhost");
String contextPath = "";
StandardContext context = new StandardContext();
context.setPath(contextPath);
context.addLifecycleListener(new Tomcat.FixContextListener());
host.addChild(context);
engine.addChild(host);
service.setContainer(engine);
service.addConnector(connector);
try {
tomcat.start();
}catch (LifecycleException e){
e.printStackTrace();
}
}
}
这段代码是一个方法startTomcat(),用于通过Java代码启动Tomcat服务器。让我解释一下这段代码的逻辑:
- 创建Tomcat实例:首先创建了一个Tomcat实例,表示要启动Tomcat服务器。
- 获取Tomcat服务器相关组件:通过Tomcat实例获取Server、Service等组件,用于配置Tomcat服务器。
- 配置连接器(Connector):创建一个Connector实例,并设置端口为8081,用于监听来自客户端的请求。
- 配置引擎(Engine)和主机(Host):创建StandardEngine实例表示Tomcat的引擎,设置默认主机为localhost;创建StandardHost实例表示Tomcat的主机,设置主机名称为localhost。
- 配置上下文(Context):创建StandardContext实例表示Tomcat的上下文,设置上下文路径为""(空字符串),并添加一个生命周期监听器Tomcat.FixContextListener。
- 组装组件关系:将上下文添加到主机上,将主机添加到引擎上,将引擎设置为服务的容器,同时将连接器添加到服务中。
- 启动Tomcat服务器:调用tomcat.start()方法启动Tomcat服务器,如果启动过程中出现异常(LifecycleException),则捕获并打印异常信息。
现在调用该类的run方法我们就可以启动Tomcat了,运行user中的调用如下:
路径参数解析
其实上面的代码,我们写的Tomcat确实运行了,但是还不可以达到我们需要的参数解析的效果,即通过你在浏览器输入的**/test 然后自动定位到我们后端应该执行的Controller,应该加上这两行**
//路径参数解析
tomcat.addServlet(contextPath,"dispatcher",new DispatcherServlet(applcationContext));
context.addServletMappingDecoded("/*","dispatcher");
这段代码是用于配置路径参数解析的部分,让我解释一下:
- tomcat.addServlet(contextPath, “dispatcher”, new DispatcherServlet(applicationContext));:这行代码的作用是向Tomcat服务器的指定上下文(contextPath)中添加一个名为"dispatcher"的Servlet,并指定Servlet实例为DispatcherServlet,构造函数参数为applicationContext。这里的DispatcherServlet通常是Spring MVC框架中的前端控制器,用于接收请求并将其分发到对应的处理器。
- context.addServletMappingDecoded(“/“, “dispatcher”);:这行代码的作用是将上一步添加的名为"dispatcher"的Servlet映射到指定的路径模式”/”。这意味着所有经过该上下文的请求都会被该Servlet处理,实现了路径参数的解析和分发功能。
也就是说,这段代码配置了一个名为"dispatcher"的Servlet,并将其映射到处理所有路径的模式"/*",实现了路径参数的解析和分发功能。通过这样的配置,请求会被DispatcherServlet处理并交由Spring MVC框架进行进一步处理,实现了请求的路由和控制。
DispatcherServlet
DispatcherServlet是Spring MVC框架中的核心组件之一,它充当了前端控制器的角色,负责接收客户端请求、委派请求处理、渲染视图等工作。
上面我们用到了该类,现在我们需要搞清楚入参applcationContext到底是什么东西
tomcat.addServlet(contextPath,"dispatcher",new DispatcherServlet(applcationContext));
我们进入该函数:
可以看到该函数接收一个Spring容器的入参,通过该容器DispatcherServlet才能对拦截到的路径请求进行映射,将其映射到SpringMVC中的controller中,也就是可以找到的Mapping
完善代码 传入容器
只需在运行该方法时加入一行,即可
完整代码如下:
public class BrSpringbootApplication {
/**
* 模拟run方法
* @param clazz
*/
public static void run(Class clazz){
//创建Spring容器 空容器没有配置
AnnotationConfigWebApplicationContext webApplicationContext = new AnnotationConfigWebApplicationContext();
//配置容器 传入配置类
webApplicationContext.register(clazz);
//启动容器
webApplicationContext.refresh();
//启动Tomcat
startTomcat(webApplicationContext);
}
private static void startTomcat(WebApplicationContext webApplicationContext) {
//使用Java代码启动
Tomcat tomcat = new Tomcat();
Server server = tomcat.getServer();
Service service = server.findService("Tomcat");
Connector connector = new Connector();
connector.setPort(8081);
Engine engine = new StandardEngine();
engine.setDefaultHost("localhost");
StandardHost host = new StandardHost();
host.setName("localhost");
String contextPath = "";
StandardContext 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(webApplicationContext));
context.addServletMappingDecoded("/*","dispatcher");
try {
tomcat.start();
}catch (LifecycleException e){
e.printStackTrace();
}
}
}
从外部调用来看
组件扫描ComponentScan
上面说到,启动类调用 BrSpringbootApplication.run(MyApplication.class); 时,会扫描传入类中的Bean及其注解,我们主要看注解中的内容,如下:
可以看到里面有组件扫描注解,于是我们先照猫画虎,给我们自己之前的模拟注解也加上该注解@ComponentScan
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@ComponentScan
public @interface BrBootApplication {
}
如果不指定该注解的扫描路径,那么默认扫描当前包及其子包
可以看到controller也位于当前包及其子包中,所以我们可以得到controller这个bean,便于后续进行Tomcat映射,至此启动项目,并且到浏览器输入访问端口,即可得到映射结果,如图:
手写多模态Webserver
鉴于有的用户需要不同的WebServer,我们需要给用户不同的选择,SpringBoot也考虑到了这一点,但是其并不是使用if进行判断,而是抽取出接口进行判断,那么我们要怎么做呢?
- 首先需要抽取出接口
- 导入依赖到pom文件,以Tomcat和jetty为例
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
<version>9.4.46.v20220331</version>
</dependency>
-
完善各自的实现类,以Tomcat和jetty为例
-
我们的启动类也要改变,不能再用之前的startTomcat()了,而是获取Webserver,采用多态的方法调用Start
/**
* 获取Webserver
* @param webApplicationContext
* @return
*/
private static WebServer getWebserver(WebApplicationContext webApplicationContext) {
//判断需要哪种webserver
//获取Bean
Map<String, WebServer> beansOfType = webApplicationContext.getBeansOfType(WebServer.class);
//判断错误情况
if (beansOfType.size() ==0){
throw new NullPointerException();
}
if (beansOfType.size()>1){
throw new IllegalStateException();
}
return beansOfType.values().stream().findFirst().get();
}
这段代码是用于获取WebServer bean 的方法,让我解释一下:
- 首先,通过webApplicationContext.getBeansOfType(WebServer.class)方法获取所有类型为WebServer的bean,并将其存储在Map<String, WebServer> beansOfType中。
- 然后,通过判断beansOfType的大小,如果没有找到WebServer类型的bean,则抛出NullPointerException异常;如果找到多个WebServer类型的bean,则抛出IllegalStateException异常。
- 最后,通过beansOfType.values().stream().findFirst().get()来获取第一个WebServer类型的bean并返回。这里为什么要使用.stream().findFirst().get()的方式呢?
- 使用.stream()将Map中的值转换为流(Stream)。
- 使用.findFirst()获取流中的第一个元素。
- 使用.get()将Optional对象中的值取出,因为findFirst()返回的是一个Optional对象,通过get()方法取出实际的值。
为什么不能直接返回 beansOfType.values()呢?其实是可以直接返回beansOfType.values(),但是要注意返回的是一个Collection而不是单个WebServer对象。如果你的业务逻辑需要获取所有WebServer对象的集合,那么直接返回beansOfType.values()是可以的。但是在这段代码中,方法的定义是返回一个单个WebServer对象,而不是集合。因此,在这种情况下,我们需要使用.stream().findFirst().get()的方式来确保我们返回的是第一个WebServer对象,而不是整个集合。
至此已经可以实现手动定义Bean来进行Webserver的选取了:
package com.bruan;
import com.bruan.webserver.impl.TomcatWebserver;
import org.springframework.context.annotation.Bean;
@BrBootApplication
public class MyApplication {
// 通过定义该bean 表示我们要选择tomcat
@Bean
public TomcatWebserver tomcatWebserver() {
return new TomcatWebserver();
}
public static void main(String[] args) {
System.out.println("程序启动");
BrSpringbootApplication.run(MyApplication.class);
}
}
或者
@BrBootApplication
public class MyApplication {
// 通过定义该bean 表示我们要选择jetty
@Bean
public jettyWebserver jettyWebserver(){
return new jettyWebserver();
}
public static void main(String[] args) {
System.out.println("程序启动");
BrSpringbootApplication.run(MyApplication.class);
}
}
模拟WebServer自动配置类
鉴于上述的手写bean进行模式选择太过于麻烦,并且使用我们框架的程序员不一定知道上述两个类的存在,也就不知道怎么去定义对应的Bean,总而言之,过于麻烦,所以我们考虑使用自动配置进行处理
- 定义自动配置类
package com.bruan.webserver.autoConfiguration;
import com.bruan.webserver.impl.TomcatWebserver;
import com.bruan.webserver.impl.jettyWebserver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 自动配置类
*/
@Configuration
public class WebServerAutoConfiguration {
/**
* 将bean定义在这里,相当于sp把我们定义好了bean
* @return
*/
@Bean
public TomcatWebserver tomcatWebserver() {
return new TomcatWebserver();
}
@Bean
public jettyWebserver jettyWebserver() {
return new jettyWebserver();
}
}
但是会出现两个bean同时生效的问题,所以需要使用condition注解进行控制bean的生效
- 条件注解的接入
可以看到需要实现condition的实现类,所以我们需要定义condition类
condition类的定义
项目结构
完成两个实现类,分别实现对Tomcat与jetty的matches方法
具体实现
- TomcatCondition
其实就是判断当前项目是否有对应的依赖 也就是是否有这个类 “org.apache.catalina.startup.Tomcat”
/**
* tomcat的接口判断类,用于判断该bean是否生效
*/
public class TomcatCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
//条件 判断当前项目是否有对应的依赖 也就是是否有这个类 "org.apache.catalina.startup.Tomcat"
try {
context.getClassLoader().loadClass("org.apache.catalina.startup.Tomcat");
} catch (ClassNotFoundException e) {
return false;
}
return true;
}
}
- 同理也可以写出 jettycondition
public class JettyCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
//条件 判断当前项目是否有对应的依赖 也就是是否有这个类 "org.eclipse.jetty.server.Server"
try {
context.getClassLoader().loadClass("org.eclipse.jetty.server.Server");
} catch (ClassNotFoundException e) {
return false;
}
return true;
}
}
依赖问题解析
问题
对于当前而言我们已经可以通过使用不同的依赖,实现对不同webserver的控制了,但是有一个严重问题是,当前我们的项目依赖是BRsp,其中包含tomcat和jetty的依赖,那么我们到底是使用了哪个呢?
解析
只需加上optional为true,
现在就只有tomcat的依赖了
切换依赖
在终依赖那里进行依赖排除,并且加入新的webserver依赖,这样就可以切换到webserver了
<dependencies>
<dependency>
<groupId>com.bruan</groupId>
<artifactId>EasySpringBoot</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.46.v20220331</version>
</dependency>
</dependencies>
空指针
如果出现空指针异常,需要在user启动类中加上import注解,因为包扫描位置不对,所以我们之前的autowebconfiguration根本就没起作用
模拟sp条件注解
springboot的ConditonalOnClass注解
@ConditionalOnClass是Spring框架中的一个条件注解,用于在特定类存在于类路径上时才会生效。当指定的类存在于类路径上时,被注解的Bean或配置类才会被注册到Spring容器中。
这个注解通常用于控制Bean的加载,可以根据类路径上是否存在某个特定类来决定是否注册某个Bean。例如,可以在一个自动配置类(@Configuration注解的类)上使用@ConditionalOnClass注解来指定只有当某个特定类存在时才加载该自动配置类。
示例用法:
@Configuration
@ConditionalOnClass({SomeClass.class})
public class MyAutoConfiguration {
// Bean definitions
}
在上面的示例中,MyAutoConfiguration这个自动配置类只有在类路径上存在SomeClass类时才会被加载。
总的来说,@ConditionalOnClass注解可以帮助开发者根据特定类的存在与否来动态地控制Bean的加载,从而实现更灵活的配置和自动化装配。
模拟实现BRConditonalOnClass注解
观察到JettyCondition和TomcatDondition很像,我们不妨进行抽象,可以抽象出代码如下:
/**
* condition的抽像总实现类,从注解BRConditionalOnClass获取值
*/
public class BruanCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
return false;
}
}
同时我们需要明确目的是实现该注解,实现输入全类名,判断数据是否存在,如果存在就生成bean,获取webserver容器
那么我们需要定义一个注解
/**
* 条件注解,优化版 可以让我们不写对应的Condition实现类
* 直接判断当前项目是否有对应的依赖 也就是是否有这个类 "org.eclipse.jetty.Server"
*/
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Conditional(BruanCondition.class)
public @interface BRConditionalOnClass {
String value();
}
同时完善上述抽取的抽象类
/**
* condition的抽像总实现类,从注解BRConditionalOnClass获取值
*/
public class BruanCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
//条件 判断当前项目是否有对应的依赖 也就是是否有这个类
Map<String, Object> annotationAttributes = metadata.getAnnotationAttributes(BRConditionalOnClass.class.getName());
String className = annotationAttributes.get("value").toString();
try {
context.getClassLoader().loadClass(className);
} catch (ClassNotFoundException e) {
return false;
}
return true;
}
}