美文网首页java学习之路
spring boot 源码解析(四)Web开发及Servlet

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

作者: 唯有努力不欺人丶 | 来源:发表于2020-12-07 23:09 被阅读0次

    说实话,这一模块是我不太喜欢的。毕竟说到使用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<Resource> 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文件的修改是这样的:
            <dependency>  
                <groupId>org.springframework.boot</groupId>  
                <artifactId>spring-boot-starter-web</artifactId>  
                <exclusions>  
                    <exclusion>  
                        <groupId>org.springframework.boot</groupId>  
                        <artifactId>spring-boot-starter-tomcat</artifactId>  
                    </exclusion>  
                </exclusions>  
            </dependency>  
      
            <!-- Jetty适合长连接应用,就是聊天类的长连接 -->  
            <!-- 使用Jetty,需要在spring-boot-starter-web排除spring-boot-starter-tomcat,因为SpringBoot默认使用tomcat -->  
            <dependency>  
                <groupId>org.springframework.boot</groupId>  
                <artifactId>spring-boot-starter-jetty</artifactId>  
            </dependency> 
    

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

     <dependency>  
                <groupId>org.springframework.boot</groupId>  
                <artifactId>spring-boot-starter-web</artifactId>  
                <exclusions>  
                    <exclusion>  
                        <groupId>org.springframework.boot</groupId>  
                        <artifactId>spring-boot-starter-tomcat</artifactId>  
                    </exclusion>  
                </exclusions>  
            </dependency>  
              
            <!-- undertow不支持jsp -->  
            <!-- 使用undertow,需要在spring-boot-starter-web排除spring-boot-starter-tomcat,因为SpringBoot默认使用tomcat -->  
            <dependency>  
                <groupId>org.springframework.boot</groupId>  
                <artifactId>spring-boot-starter-undertow</artifactId>  
            </dependency>  
    

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

    嵌入式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

        本文链接:https://www.haomeiwen.com/subject/mycwwktx.html