spring boot 源码解析(四)Web开发及Servlet原理

说实话,这一模块是我不太喜欢的。毕竟说到使用Spring Boot我少说也有两年的经验了。我看这个教材主要就是为了源码解析这一块的东西。不过既然讲到了,还是看一遍吧。也希望有一些惊喜。

Spring Boot的使用

这个其实大概流程就是:

  1. 创建Spring Boot应用,选中我们需要的依赖(模块)
  2. Spring Boot已经默认将真写场景配置好了,只需要在配置文件中指定少了的配置就可以运行起来。
  3. 自己编写业务代码。

其实说到底Spring Boot还是要说自动配置原理。这个几乎每一章都要说到。比如说加入我们引入了jdbc这一个包。会触发jdbc自动配置。如下图:


datasource自动配置

再次总结一下(我感觉说了有三次了):
Spring Boot中:
xxxxAutoConfiguration:帮我们给容器中添加配置组件
xxxxProperties:配置类来封装配置文件的内容(一般会和配置文件绑定)

Spring Boot对静态资源的映射

这个说起来自从用了Spring Boot一直都是前后端分离工作。这个对我来说还真的是个新知识。以前spring的时候是有个web-app文件夹的。但是Spring Boot是没有的。其实这块我们可以自己去看看Spring Boot的静态资源放哪里(我觉得自己去找放哪里比知道放哪里本身更有意义。):

源码截图

图上很明确的说了,所有/webjars/,都去classpath:/META-INF/resources/webjars/找资源。**
webjars:以jar包的方式引入静态资源
关于这个我们可以去官网看一下:https://www.webjars.org/
webjars

简单来说,这个webjars就是把我们常用的前端框架封装,我们可以根据不同版本和不同形式(比如maven或者gradle)直接引入到项目中。
引入jquery后的目录结构

在访问的时候直接写webjars下资源的名称就行。
而我们项目内的静态资源默文件夹默认有四个,依旧源码说话:
静态资源文件夹

如上图,都是类路径下的,反正常用的static和resources。public见名知意。META-INF/resoureces符合spring的感官。
项目目录

如上图,我什么额外的配置都没有,直接访问的话
直接访问类路径下面的static资源

还有一个值得提的方法,就是欢迎页。如下如:
欢迎页的映射

这个代码其实我大部分看不懂,但是挑能看懂的看。明晃晃那么特别的蓝色字符串:index.html还是很容易看到的。稍微有点部署经验的应该知道这个一般就是项目的入口。

        private Optional getWelcomePage() {
            String[] locations = getResourceLocations(this.resourceProperties.getStaticLocations());
            return Arrays.stream(locations).map(this::getIndexHtml).filter(this::isReadable).findFirst();
        }
private Resource getIndexHtml(String location) {
            return this.resourceLoader.getResource(location + "index.html");
        }

        private boolean isReadable(Resource resource) {
            try {
                return resource.exists() && (resource.getURL() != null);
            }
            catch (Exception ex) {
                return false;
            }
        }

简单的看一下这个代码,获取所有静态资源文件夹。然后从这里流计算获取存在的index.html结尾的第一个页面。而且这里还有一个小细节:这四个静态类是有优先级的。顺序就是上面静态资源的数组的顺序。看下图:


public和static都有index.html

其实这个很好猜,打印的是static中的html。


运行结果

手下,大家稍微想一下就能发现,之前的流计算是获取第一个。而这个顺序是把这四个资源地址依次传入的,static是下标2的第三个。public是下标3的第四个,所以static在public前面,这个我们还可以再加一个试验下:
项目目录

运行结果

其实这个是个很有意思的事。当然了,如果四个目录下都没有index.html就会报404错误,这个就不说了。
当然了,回到最基本的,这四个静态资源文件夹是Spring Boot 默认的,而我们之所以看这么多就是为了能够读懂代码,自己配置,接下来我们自己配置一下警惕资源路径吧。


image.png

回到这个类我们可以看出,这个配置是spring.resources下的。我们使用这个试一下:
自己配置静态路径

springBoot的默认配置失效

其实这两张图很明显,自己定义了静态资源路径后,原本SpringBoot默认的就失效了,所以我这里访问不到首页了。
而我自己定义的路径是可以访问的了

国际化(不同语言配置)

这个其实我之前工作也做过这种需求,不过因为这个教程上也提到了这个需求,我就再简单的说一下。
因为这个教程上是个前后端一起的项目,所以它是直接在项目里配置的不同语言的不同绑定。但是其实这个在前后端分离的项目中,也是差不多的实现。
大概步骤就是每一个需要根据语言而切换的属性都有一个唯一的id。然后把id和对应的值作为一个key-value对存储。因为这个功能不管是网页还是app,其本质都是一堆很多的kv对,所以这里建议的是用文档的形式传入后端。再根据用户的不同选择返回不同的文件。
而且这个文件是变化的。比如随着业务功能,可能项目功能也有变化,多个页面之类的,这样相对的kv对是增加了的。
而且也有可能一开始是支持中文/英文。 但是随着产品的发展,后期需要日文,法语,德语等。
所以我个人的建议就是一切写活(写活的意思就是增删改都交由后台管理员操作):
所以我这里说一下我的想法:
创建一个数据库表。表中字段id,语种,文件名。
在运营端这个应该是列表的形式展现的。可以增加和修改(删除可以酌情是否允许。如果允许要注意这个语种有人选择了要怎么操作)。然后这个文件是可以被替换的。我当时的实现是对这个文件格式做校验。严格要求kv对。不管是properties还是excel都可以。然后格式正确的就将文件存到电脑的指定位置。并把文件名称存到数据库中。
前端用户选择了不同的语种,会默认的将这个语种对应的文件下载到本地。页面显示也会解析这个文件显示出来。

Restful接口

其实这个没啥神奇的。就是以前每个接口都要有一个单独的名称。但是如果是restful接口风格,就是可以相同操作对象的接口用一个对象。
比如说之前一个用户的增删改查。需要四个接口:

  • xx/addUser
  • xx/editUser
  • xx/delUser
  • xx/userList

反正这四种接口命名是我习惯的命名方式。
但是如果用restful接口风格的话,可以就只用一个接口命名:xx/user

  • get方式请求xx/user是获取用户列表
  • post方式请求xx/user是修改用户
  • put方式请求xx/user是添加用户
  • del方式请求xx/user是删除用户

说到底是可以让接口设计看上去更见优雅简单的一种设计风格,说多神奇也不至于。如果稍微有点经验的程序员应该知道spring(Spring Boot)中是不能有一样的接口路径的。但是其实不同请求方式的接口路径是可以的。

Spring Boot的错误处理机制

当我们访问一个系统不存在的接口时,会有个默认的错误页面:


访问不存在的路径

如果不是浏览器,是接口访问(这里可以用postman自测)会返回的是错误信息。
而这个页面/错误信息是怎么出现的呢?不用想肯定是Spring帮忙做的啊。但是问题是我们不想要这样的,所以要怎么做才能解决这个问题?
这里首先要去找到Spring是怎么做的:
继续去源码中找原因,我们会发现error是有专门的包的:


error默认配置的类

点进入这个类我们会发现里面注入了好多个bean。一个个看有点太费神了,这里根据讲师说的主要的四个bean介绍下:
  • DefaultErrorAttributes
  • BasicErrorController
  • ErrorPageCustomizer
  • DefaultErrorViewResolver

下面具体的说下每个bean的作用。
ErrorPageCustomizer:一旦系统出现4XX或者5XX之类的错误,ErrorPageCustomizer就会生效,来到/error请求。进入到BasicErrorController中的/error方法中。
BasicErrorController:这个类中的方法很有意思,有两种请求的处理方式,路径一样但是会返回两种结果,一种是产生html结果,还有一种是json的。这也就是浏览器返回页面,接口请求返回json的方法(这个原因是浏览器发送请求会在请求头标注想接受text/html)。

两种返回结果

DefaultErrorViewResolver:这个就是用来找到这个这个error页面的。
根据错误去找不同的错误页面

如上图有模板引擎可以直接用。但是没有的话回去静态资源路径下找error.html页面。
(ps:分析了一大堆,这个教程中老师完美的为自己的项目做好了错误页面指向,然后我一脸懵逼,因为所有前端的东西我都跳过了。。就当学一波分析源码吧,这一块异常的处理就跳过了)

配置嵌入式Servlet容器

Spring Boot自带的嵌入式Servlet是tomcat这个在一开始就讲了。但是这个servlet的默认配置可以怎么设置呢?而且能不能修改不用tomcat呢?下面一个个说。

配置文件修改servlet

首先其实简单的修改servlet默认配置其实我们大多都用过。之前为了测试配置文件的优先级就是用不同的端口测试的。在配置文件种server.port=xxx改的。如果稍微有点工作经验的应该也知道统一路径前缀什么的server.context.path=/xxxx.总而言之这个关于servlet的默认配置应该都是server开头的。更具体的我们可以去源码看看


tomcat配置

其实这个ServerProperties类都是关于servlet的配置,内容比较多。其中有关于tomcat的配置。还有一些别的东西。我们在类种可以看到是绑定配置文件种server开头的配置的。所以几乎这个类中的属性都是可配置的。如果我们想修改默认配置可以在配置文件中修改相应的属性。

代码修改servlet默认配置

当然了,这里不仅仅可以通过配置文件修改servlet,也可以用别的方式。比如在编码中更改配置,如下图

编码中更改端口

其实这块因为我看的教程是SpringBoot1.5的。所以老师讲的那个类已经莫得了,所以我翻了半天可能的类才找到这个用到的类。
而且我看弹幕也有好多说2.1版本是另一个配置类,所以我充分怀疑这个类可能会总变。而且换句话说,为什么配置文件能解决的小问题非要在编码中秀技呢,这里反正不看好这种方式。
注册Servlet,Filter,Listener
SpringBoot提供三种方式:ServletRegistrationBean,FilterRegstrationBean,ServletListenerRegistrationBean
注册Servlet
到实际代码中,写一个自己的servlet一般都继承HttpServlet。重写它的方法(我这里只简单的重写两个)
继承HttpServlet重写get/post方法

到这里我们把一个简单的Servlet写完了,但是要想起作用还要把它注册到容器中。下面是把这个Servlet注册到容器的过程(ps:图上注释有问题,想要全拦截是/而不是/*.刚刚实际试了下)
注册这个容器并配置拦截路径

到这里,我们启动项目并访问/test接口:
是我们想要的结果

至此,说明我们自己的servlet起作用了。
同理另外两个组件也是这样注册。下面简单的说一下:
注册Filter
同样先写一个自己的过滤器

然后把这个过滤器注册:
说明过滤器起作用了

其实这个注册的方法和servlet的注册差不多,这里的参数建议点进去看看能传什么就好啦。当然了如果不想拦截指定的servlet,也可以单独设置url,用法如下:
单独设置拦截路径

按照代码逻辑,应该是访问test的时候进入拦截器,访问test1的时候不进入,下面我们两个都访问试试。
试试证明确实是只有访问test的时候才会进入到拦截器,所以说这里设置拦截器比较灵活,可以指定某个servlet,也可以单独指定路径。
注册Listener
最后说一下注册监听器,因为监听器有很多,这里用监听容器启动和销毁的ServletContextListener举例。
我的这个监听器其容器启动和销毁的时候分别打印一句话

然后把这个监听器注册:
注册我的监听器

这个就比较简单了,就把监听器传入就可以了。
然后启动项目,会发现确实起作用了:
我这里为了显眼特意错误打印的红色

其实对于这三大组件的一些配置,都可以在这个RegistrationBean中配置,至于具体可以配置什么建议看源码。因为这几个类的源码都不是很复杂。
而实际上,SpringBoot在我们项目启动的时候会自动把SPringleMVC中的DispatcherServlet注册进来,下面我们可以去看看这个servlet的配置。

springboot自动把dispatcherServlet注册进来

分析一波源码,我们可以看到这个dispatcherServlet设置的拦截的路径是:webMvcProperties.getServlet().getPath().我们继续往下找,会发现这个path默认又是/。所以说实际上这个会默认拦截所有的请求,包括静态资源。
默认拦截所有请求

当然了,看到这里也就知道,我们可以通过修改server.servletPath来修改SpringBoot的默认拦截路径啦。
关于servlet组件注册就说到这里,下面说第二个问题。

如何不适用默认的Tomcat而使用其他的Servlet容器

其实这个我们在上面看源码的时候应该就看到一些了。配置文件中除了tomcat还有其他的容器哟!


ServerProperties源码

但是怎么使用其他的呢?(ps:这里要先说下,undertow不支持jsp的,它是一个高性能非阻塞的容器,Jetty是适合长连接的。)
SpringBoot默认支持:Tomcat(默认使用),Jetty,Undertow。
至于怎么从Tomcat切换成其他的呢,其实也简单的很,其实Boot项目会默认引入Tomcat包,这里我们只要把Tomcat依赖删除,引入别的包就行了。


如下代码

pom文件的修改是这样的:
          
            org.springframework.boot  
            spring-boot-starter-web  
              
                  
                    org.springframework.boot  
                    spring-boot-starter-tomcat  
                  
              
          
  
          
          
          
            org.springframework.boot  
            spring-boot-starter-jetty  
         

挺简单的逻辑,直接修改pom就可以啦,而且之前的设置端口是12也还是生效的。
同理,修改成undertow也是这样。下面的undertow的pom:

   
            org.springframework.boot  
            spring-boot-starter-web  
              
                  
                    org.springframework.boot  
                    spring-boot-starter-tomcat  
                  
              
          
          
          
          
          
            org.springframework.boot  
            spring-boot-starter-undertow  
          

切换其实挺简单的,但是其原理什么呢?下面来一波源码分析。

嵌入式Servlet容器自动配置原理

这里先科普个小知识:embedded--->嵌入的


embedded嵌入的

因为这个单词反正我不认识,所以刚刚百度翻译翻译过来的,下面我们就根据这个单词去查找源码(感觉web这块的源码都说的腻歪了,autoconfigure下的web包下):


要查看的源码地址

点进去瞅一瞅代码很简单,也就是单纯的注入bean,但是这个注入条件很有意思:
自动注入嵌入式容器配置

看到这大家应该就能很清楚了,能根据不同依赖注入的原理就是不同的依赖中有其独有的类,当这个类存在的时候才会注入bean。
这个也就是为什么切换Servlet容器的时候先要排除tomcat。不然的话会默认使用Tomcat。而且如果两个容器同时启动可能会有莫名其妙的问题。
咱们继续说把servlet容器作为bean注入到spring容器中:


注入后不同的bean会自己去找配置

然后这个时候这个容器差不多就准备好了(中间省略了很多过程,反正知道这个时候的servlet容器是拿到我们的配置了就行)
然后这个容器就在这准备着。
当SpringBoot项目启动的时候下面一步一步用图的方式走一遍流程:
1.启动SpringBoot项目,进入run方法

刚刚框起来的方法会选择我们用什么方式启动,比如servlet或者reactive等。正常我们这条线会servlet启动。
2,进入refreshContext方法里

3.refresh方法往下找,找这个父类的方法实现

4.进入到实现中,往下找发现调用了创建webserver方法。

5.进入创建webserver方法后选择我框起来的查看实现类

至此,我们终于看到点和tomcat有关的东西了。


点进Tomcat的实现

下面因为在一个类中跳来跳去的,所以一步一截图:
j进入getTomcatWebServer方法

进入TomcatWebServer类

发现类构造器中有个初始化方法,往下看,初始化的时候顺便start了

这一串图终于让我们把SpringBoot的启动和Tomcat的启动整合在一起了。

使用外置的Servlet容器

其实这个怎么说呢,SpringBoot内置的Servlet容器大大简化了开发,而且打个jar就能跑,简单又便捷。
可是其缺点其实也很明显:比如默认不支持JSP,优化起来也比较复杂(配置其实都很绕,具体能配置什么还要去源码找,如果自己编写嵌入式Servlet容器的创建工厂又太过于复杂,对技术要求较高)。
总而言之,其实SpringBoot也可以使用外置的Servlet容器。其实这个也很简单,把创建项目以war包的形式创建就可以了。
不过这个创建完了一般是没有webapp的,建议自己手动创建一下。web.xml也是如此。
然后可以把tomcat整合进编译器,直接跟普通项目一样放在tomcat里跑就行了。其实到这里SpringBoot项目和普通项目就一样了,所以不多说了(毕竟这一块我是真不熟)总结一下要怎么用外部Servlet:

  1. 必须是war项目。
  2. 将嵌入式Tomcat指定为provided
  3. 必须。调用configure方法


    传入主程序类
  4. 这个war就可以正常使用了。编写一个继承SpringBootServletIntializer的类
外置Servlet原理

这个与内置的jar正好相反。

  • jar包:执行SpringBoot的主类main方法,启动ioc容器,创建嵌入式的Servlet容器并启动
  • war包:启动服务器,服务器启动SpringBoot应用,启动ioc容器
    重点就是这个服务器启动SpringBoot应用。war包启动的流程:
  1. 启动tomcat
  2. 在项目的依赖中找到如下文件。


  3. 根据这个文件中的全类名找到这个类,将@HandlesTypes标注的所有这个类型的类都传到onStartup方法的set中,为这些都创建实例。
  4. 每一个类都调用自己的onStartup方法。
    需要注意的是,上面说了一定要编写一个继承SpringBootServletIntializer的类。而这个SpringBootServletIntializer类就是第三步说的@HandlesTypes标注的类。
  5. 我们自己创建的那个继承的类,会执行onStartup方法,其中我们自己的类会重写configure方法,传入的就是我们SpringBoot项目的主类。
  6. 在onStarpup方法中,就会创建这个Sping应用,并且在方法的最后run了。
  7. 而这个run其实就是我们主类的run方法。至此,就变成SpringBoot的正常启动了。
    下面是一个整理过的截图:


    外置容器启动原理

至此,所有SpringBoot的web部分就算学完了,其实还是囫囵吞枣,暂时我的计划是这个教程学完了再去啃书。其实我感觉 的重点就是Servlet容器这一块,可能还比较实用。不管是三大组件注册还是切换不同的Servlet容器,还是内置容器启动的原理,都挺有意思的。前面很大篇幅讲了一些前端,SpringMvc的东西。有用肯定是有用的,但是比较从工作就一直前后端分离的我来说有点不想学。所以中间也跳过一部分。反正历经三四天,这块东西也都算过了一遍,这篇笔记算是终于整理完了。
本篇笔记就记到这里,如果稍微帮到你了记得点个喜欢点个关注,也祝大家工作顺顺利利!最近看到一句很好的话:除非付出行动,否则口说无凭,没有人能通过祈祷改变人生!这句话也送给正在学习的大家,我们共勉!

你可能感兴趣的:(spring boot 源码解析(四)Web开发及Servlet原理)