前言
SpringBoot是目前最广泛的Web开发框架,其极度方便的开发特性得到市场的充分认可。这一系列的博客会进行一个详细的探讨,包括SpringBoot的原理模拟,SpringBoot源码解读,以及SpringBoot重要功能的官方文档的解读。俗话说了解一个组织就到打入这个组织内部,毕竟我就看一眼嘛。所以笔者会在Spring框架下,模拟一个SpringBoot的基本功能入手,然后在和SpringBoot的源码做对比,深度学习一下SpringBoot。本篇将会探究SpringBoot是怎么实现从开发到集成Tomcat一键完成,又是如何直接读到静态资源的,本篇结束以后将会后续进行SpringBoot源码的解读,深度对比SpringBoot做了哪些更深一层的功能。更多Spring内容进入【Spring解读系列目录】。
添加依赖
当创建一个Maven-Web项目以后,首先自然要把Spring的包和Web相关的包添加进来。
<!--Spring 上下文包-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.8.RELEASE</version>
</dependency>
<!--Spring web依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.2.8.RELEASE</version>
</dependency>
<!--servlet依赖-->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
构建Spring MVC框架
其实这一部分笔者已经在【SpringMVC新版本(Spring 5.3.0)官网详解】博客里面详细的有过解释。所以这次就不多赘述,直接构建出来:配置类AppConfig,初始化类MyWebApplicationInitializer
,完成一个Spring MVC的基本配置。
@ComponentScan("com.demo")
@Configuration
public class AppConfig {}
}
public class MyWebApplicationInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
AnnotationConfigWebApplicationContext anno=new AnnotationConfigWebApplicationContext();
anno.register(AppConfig.class);
anno.refresh();
DispatcherServlet servlet=new DispatcherServlet(anno);
ServletRegistration.Dynamic registration = servletContext.addServlet("myBootServlet", servlet);
registration.setLoadOnStartup(1);
registration.addMapping("/");
}
}
构建Web入口
SpringBoot是通过一个main()
方法直接调用run()
方法来的Web配置和Tomcat服务器。那么就把Web入口先创建出来MyBootApplicationStart
,在它的main()
方法里调用启动器。
public class MyBootApplicationStart {
public static void main(String[] args) {
MySpringBootApplication.run();
}
}
构建启动器
这一步就要开始构架启动器的run()
方法了。SpringBoot在run()
方法里启动了Tomcat,所以我们也得这么做。
public class MySpringBootApplication {
public static void run(){
}
}
很明显此时应该在里面做Tomcat的启动,必然需要引入Tomcat相关依赖。因为需要new
一个Tomcat并启动,就是这么简单。
<!—Tomcat核心-->
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>9.0.13</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-el</artifactId>
<version>9.0.13</version>
</dependency>
<!—Tomcat JSP支持-->
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<version>9.0.13</version>
</dependency>
构建Tomcat
那么就直接new
一个Tomcat,然后直接start()
启动,Tomcat跑起来了,完事儿。
public class MySpringBootApplication {
public static void run(){
//new一个tomcat
Tomcat tomcat=new Tomcat();
//设置端口号
tomcat.setPort(9098);
try {
//启动
tomcat.start();
//阻塞等待连接进入
tomcat.getServer().await();
} catch (LifecycleException e) {
e.printStackTrace();
}
}
}
******************9.0.13启动Log:******************
Nov 09, 2020 5:52:28 PM org.apache.catalina.core.StandardService startInternal
INFO: Starting service [Tomcat]
******************8.5.5启动Log:******************
Nov 09, 2020 5:55:24 PM org.apache.coyote.AbstractProtocol init
INFO: Initializing ProtocolHandler ["http-nio-9098"]
Nov 09, 2020 5:55:24 PM org.apache.tomcat.util.net.NioSelectorPool getSharedSelector
INFO: Using a shared selector for servlet write/read
Nov 09, 2020 5:55:24 PM org.apache.catalina.core.StandardService startInternal
INFO: Starting service Tomcat
Nov 09, 2020 5:55:24 PM org.apache.coyote.AbstractProtocol start
INFO: Starting ProtocolHandler [http-nio-9098]
没错就是这么简单,但是现在我们启动的Tomcat非常的简单,既不能读取Jsp或者html这些静态资源,也不能加载SpringMVC的配置,只是把Tomcat启动了而已。此外,博主把8.5.5启动Log
和9.0.13启动Log
都贴出来,还是推荐大家使用8.5.5
作为学习的jar包,因为打印的日志更多。
完善静态资源访问
为了完善项目,我们需要给Tomcat加上一些必要的功能。首先要指明Class文件放在哪里,一般来说就是/src/main/java
目录,这个只要指定好项目目录就可以做到。那么重要的就是要指明静态资源放在哪里,就是SpringBoot的/resources
目录,其下还有三个子目录/resources
,/static
和/public
。
回顾一下SpringBoot的功能,就能发现要模拟SpringBoot面临一个很重要的问题,就是上面说的访问资源的问题。因为当新建一个SpringBoot项目以后,只要静态文件放到上述文件夹里面就可以直接访问,如同访问Java文件一样方便。Java文件还可以理解毕竟可以交给Spring去管理。但是静态文件是如何访问的呢?比如就可以直接访问index.html
静态页面。
其实我们可以有这样的思路:首先一个内容如果要被解析,那么它一定要被加载到内存中去,然后才能被操作。那么当接收到一个请求的时候,就可以截取其后缀,然后根据项目路径拼出来文件路径。有了路径+文件名字以后,在项目重new一个文件对象出来。之后通过FileInputStream
读到内存中,最后通过response
响应输出出去。
重写DispatcherServlet
为了完成上面的构想,就需要重写DispatcherServlet
,由我们自己的Servlet
去完成拦截请求的功能,那么我们在上面MyWebApplicationInitializer.onStartup()
方法里写的代码基本就作废了。这里需要重写ServletContainerInitializer.onStartup()
方法。关于这俩的区别可以参考【SpringMVC源码Tomcat如何访问WebApplicationInitializer】。那么我们自己的DispatcherServlet
就命名为MybootServlet
。
public class MybootServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//得到静态资源后缀"/index.jsp或者/index.html",由于这些文件内容在编译后都会放在classpath下。
// 因此得到classpath也是必须的
String fileName=request.getRequestURI();
//得到classpath
String basePath=MybootServlet.class.getResource("/").getPath();
//拼出来文件路径完整路径,此时我们拿到的类似于D:/项目名/src/java/main/index.html这样的
String filePath=basePath+fileName;
//构造一个file对象,把静态资源读到file对象里
File file=new File(filePath);
//InputStream读入内存
InputStream inputStream=new FileInputStream(file);
byte[] bytes = new byte[1024];
inputStream.read(bytes);
inputStream.close();
//创建相应内容
String str=new String(bytes);
//设置响应内容
response.setContentType("text/html");
//写出去
response.getWriter().write(str);
}
}
重写Initializer
自己的Servlet
有了以后还需要注册到Tomcat里才行,因此还要重写Initializer
。
public class MybootInitializer implements ServletContainerInitializer {
@Override
public void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException {
//注册我们自己的Servlet到Tomcat里
ServletRegistration.Dynamic registration = ctx.addServlet("mybootServlet", new MybootServlet());
//拦截所有的请求。
registration.addMapping("/");
}
}
最后还需要配置一下SPI,什么是SPI【SpringMVC源码】链接里面有介绍。
创建出来文件:javax.servlet.ServletContainerInitializer
配置上自己的服务:com.demo.app.MybootInitializer
以及一个web测试文件index.html
。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
asdfadasdasdasdasd
</body>
</html>
运行查看结果,我们静态资源就被输出在网页上了
总结
本篇博客基本上模拟了一下SpringBoot是如何启动Tomcat,以及如何读取到静态资源的。本篇博客最重要的一个点,其实在于使用Servlet3.x添加的SPI功能进行了一个Tomcat外部服务的插入功能。后面笔者还会针对SpringBoot在这一块的源码做一个解读对比,看看大神们构思的SpringBoot和模拟的在思路上有什么区别。
附:完整的路径以及代码
路径
AppConfig
@ComponentScan("com.demo")
@Configuration
public class AppConfig {
}
MybootInitializer
public class MybootInitializer implements ServletContainerInitializer {
@Override
public void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException {
ServletRegistration.Dynamic registration = ctx.addServlet("mybootServlet", new MybootServlet());
registration.addMapping("/");
}
}
MySpringBootApplication
public class MySpringBootApplication {
public static void run() {
String relativePath=System.getProperty("user.dir");
//拿到项目路径,也就是当前项目的target\classes目录,项目编译后的路径
String sourcePath=MySpringBootApplication.class.getResource("/").getPath();
String webAppDirLocation=relativePath+"/src/main/webapp";
Tomcat tomcat=new Tomcat();
//tomcat.setBaseDir();
tomcat.setPort(9080);
try {
//告诉tomcat,源码在哪里,得到Context
StandardContext ctx = (StandardContext) tomcat.addWebapp("/",new File(webAppDirLocation).getAbsolutePath());
//class文件所在的位置
WebResourceRoot resourceRoot=new StandardRoot(ctx);
//源码的路径resourceRoot,发布后的路径/WEB-INF/classes,项目编译后的路径sourcePath
resourceRoot.addPreResources(new DirResourceSet(resourceRoot,"/WEB-INF/classes",sourcePath,"/"));
ctx.setResources(resourceRoot);
//启动Tomcat
tomcat.start();
//等待连接进入
tomcat.getServer().await();
} catch (LifecycleException | ServletException e) {
e.printStackTrace();
}
}
}
MyBootApplicationStart
public class MyBootApplicationStart {
public static void main(String[] args) {
MySpringBootApplication.run();
}
}
MybootServlet
public class MybootServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String fileName=request.getRequestURI();
String basePath=MybootServlet.class.getResource("/").getPath();
String filePath=basePath+fileName;
File file=new File(filePath);
InputStream inputStream=new FileInputStream(file);
byte[] bytes = new byte[1024];
inputStream.read(bytes);
inputStream.close();
String str=new String(bytes);
response.setContentType("text/html");
response.getWriter().write(str);
}
}