深入剖析Tomcat(十、十一) 详解StandardWrapper

《深入剖析Tomcat》第十章介绍了Tomcat的安全机制,主要就是对servlet的访问做安全验证,如果Tomcat中设置了某些servlet需要指定角色的用户才能访问,则需要客户端进行登录验证,如果用户名密码正确并且该用户拥有该角色的话,Tomcat就会放行,否则就会返回403(无权访问)。用户名密码及角色配置是需要配置在Tomcat内部的,通常是放在一个叫做“tomcat-users.xml”的文件中。

鉴于当前java项目开发基本都在使用springboot框架,也不需要关注Tomcat本身的安全性问题(项目的权限验证由项目自身来实现),大家也没必要去给Tomcat去配用户名密码角色。所以这块内容就不再深入了解了,直接步入下一章:详解StandardWrapper。

StandardWrapper的基本流程

StandardWrapper是Wrapper容器的标准实现,Tomcat中使用的就是这个类,Wrapper容器是对一个servlet的包装,主要功能就是要去调用某个特定servlet的service方法。

所以不难想到,StandardWrapper首先要知道它持有的servlet是哪个,实现方式是它持有一个【String servletClass】属性,用来存放servlet的全限定名,通过 setServletClass(String servletClass) 方法可以设置这个属性值。

有了servlet的全限定名,StandardWrapper要想获取到一个servlet的实例,就必须要先对这个servlet进行类加载,要进行类加载就必须要拿到一个类加载器,而在Tomcat中类加载器是被包含一个叫做"加载器"的组件中的,加载器通常是 WebappLoader(实现了Loader接口)这个类的一个实例,StandardWrapper拿加载器的逻辑是这样的:先看自己有没有,没有的话就去拿父容器(Context容器)的,所以通常来说Tomcat会给Context容器分配一个加载器,Context底下的Wrapper小弟们都用它的就行。

public Loader getLoader() {

    if (loader != null) return loader;
    if (parent != null) return parent.getLoader();
    return null;

}

拿到 WebappLoader 实例后就拿到了 ClassLoader 实例(类加载器),下一步很显然就是对servlet进行类加载操作,这个很简单,直接 ClassLoader.loadClass(String name) 方法就能把类加载好了,然后就是反射操作 newInstance 创建一个servlet的实例,接着servlet.service() 方法一调用,StandardWrapper的使命就基本完成了。

了解了StandardWrapper的基本工作流程后,再来看看它里面比较重要的两个设计 SingleThreadModel过滤器。

SingleThreadModel

javax.servlet.SingleThreadModel 是一个接口,没有任何方法定义,只是一个标识,用修饰servlet的具体实现类,如果一个servlet没有实现SingleThreadModel这个接口,那么在Tomcat的整个生命周期内,针对该servlet对象的StandardWrapper实例就只创建一个servlet实例,每个线程都公用这一个实例,允许并发调用。也就是说该servlet是单例的。

如果一个servlet实现了SingleThreadModel接口,那么多个线程要使用该servlet时,StandardWrapper就会为每个线程分配一个servlet实例,一个servlet实例在同一时刻只会被一个线程调用。Tomcat为了避免重复创建servlet对象耗费资源,所以在每个StandardWrapper实例中都维护了一个servlet对象池,一个线程要使用servlet对象时,就从对象池里拿一个给它用,如果池子里没有了就创建一个新的;线程使用完servlet对象后,StandardWrapper会再将其回收到对象池里。也就是说该servlet是多例的。

如果一个servlet实现了SingleThreadModel那么该servlet就是线程安全的吗?

这是一个认知误区?何为线程安全,那就是多线程并发调用的情况下,公共资源的完整性和逻辑正确性不会遭到破坏,公共资源就是多个线程都会用到的资源。而SingleThreadModel的实现逻辑只是保证了每个servlet内部持有的属性不会被其他线程破坏,但是如果该servlet用到了其他类里面的属性,那么仍然是存在线程安全问题的。

注意!SingleThreadModel这个接口在Servlet API 2.4版本(对应Tomcat 5.5.x版本)后已经被打上了 @Deprecated 标记了,不再推荐使用,但是代码逻辑仍然保留着。

上面提到的StandardWrapper的类加载、创建sevlet实例及SingleThreadModel的逻辑基本都包含在 StandardWrapper#allocate() 这个方法中,allocate() 方法的目的就是返回一个可用的 servlet 实例。

过滤器

过滤器的逻辑其实并不在StandardWrapper中,而是在它的基础阀 StandardWrapperValve 中。

先说一下StandardWrapperValve的作用。

  1. 将Catalina接收到的request与response对象转换成servlet可用的HttpServletRequest与HttpServletResponse。
  2. 调用StandardWrapper的 allocate() 方法获取到一个servlet实例。
  3. 构建过滤器链 FilterChain。
  4. 执行所有过滤器的doFilter方法,最后调用 servlet 的 service() 方法。

过滤器链 FilterChain 又是一个“责任链”机制的应用,Tomcat中用到责任链的地方还挺多,每个容器中的 Pipeline 也是应用的“责任链”调用机制。不了解 Pipeline的同学可以看看第五章的文章【深入剖析Tomcat(五) 剖析Servlet容器并实现一个简易Context与Wrapper容器】

FilterChain的调用过程大致如下图

如何自定义过滤器并加入到过滤器链中呢?

首先需要有一个自定义过滤器的类,这个类需要实现 javax.servlet.Filter 接口,比如我自定义的过滤器叫MyFilter1

public class MyFilter1 implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        System.out.println("MyFilter1   start");
        chain.doFilter(request,response);
        System.out.println("MyFilter1   end");
    }

}

MyFilter1的前置逻辑和后置逻辑都比较简单,就是简单输出了一个字符串。

然后怎么使这个类生效呢?也就是说怎么做才能将这个类加到Wrapper容器的过滤器链中呢?

两种方式(基于Spring框架):

1.声明一个FilterRegistrationBean(推荐使用)

这种方式需要为自定义过滤器类创建一个bean,Tomcat的Wrapper容器在构建过滤器链时会找到这个bean,并将其加入到过滤器链中,同时声明这个Bean时也可以为它设置排序值order,过滤器链中的过滤器都是按其order值从小到大排序的。

@Configuration
public class ServletConfiguration {

    @Bean
    public FilterRegistrationBean<MyFilter1> myFilter() {
        FilterRegistrationBean<MyFilter1> registrationBean = new FilterRegistrationBean<>();
        registrationBean.setFilter(new MyFilter1());
        registrationBean.addUrlPatterns("/servlet/*");
        registrationBean.setOrder(2);

        return registrationBean;
    }

}

2.使用@WebFilter注解

在自定义Filter类上加上 @WebFilter 注解,然后Spring启动类上加上 @ServletComponentScan 注解, Wrapper容器也能找到它并将其纳入过滤器链中,不过缺点是不能自定义排序值,order值永远是2147483647(Integer.MAX_VALUE)。有的人说配合spring的 @Order 注解就能排序了,其实不然,说这话的人肯定也没有实践过。

@WebFilter(urlPatterns = "/servlet/*")
@Order(100)  // 这个注解在这里不生效,没什么作用
public class MyFilter1 implements Filter {
    ………
}

综合上面两种方式,我还是推荐第一种:FilterRegistrationBean 的方式,毕竟给过滤器排序在很多场景下都是需要的。

源码分析

在Tomcat中,众多过滤器其实是挂在Context容器下的,StandardContext类下有个 filterMaps 属性,该属性存放了与该Context容器相关联的多个过滤器,SpringBoot在启动时会搜寻所有过滤器并将其加入到 filterMaps 属性中。当servlet请求打进来后,Wrapper容器在组装过滤器链时会去Context容器中取 filterMaps 属性来拼装过滤器链。

接下来基于SpringBoot框架来看看过滤器生效的过程

首先启动SpringBoot,ServletWebServerApplicationContext#selfInitialize 方法中会调用 getServletContextInitializerBeans() 方法来获取所有过滤器类(我们自定义的过滤器生不生效在这里就可以看出来了),这时候就能看到我们自定义的过滤器已经被检索到了

接下来for循环中执行方法“beans.onStartup(servletContext)” ,该方法逻辑中会调用到StandardContext的 addFilterMapBefore() 或者addFilterMap() 方法,这两个方法都是给Context容器添加过滤器,逻辑大致相似,区别是将过滤器往过滤器链的头上加还是尾上加,自定义过滤器会通过 addFilterMapBefore 这个方法被添加进来。注意看这里就是往 StandardContext 的 filterMaps 属性中放值的过程。

SpringBoot启动完后,StandardContext中的filterMaps 也就放好值了。

然后这时候来了一个servlet请求,通过Context的Pipeline和Wrapper Pipeline的流转,请求来到了Wrapper的基础阀:StandardWrapperValve,StandardWrapperValve的invoke方法中会去构建一个过滤器链的对象

ApplicationFilterFactory.createFilterChain方法中会取StandardContext中的 filterMaps 属性来构建过滤器链,构建过程就不展示代码了,感兴趣的可以翻翻这块的代码。

filterChain 对象构建好后,还是在StandardWrapperValve的invoke方法中,执行 filterChain 的 doFilter 方法,该方法会按照顺序来依次执行各个过滤器的 doFilter 方法,在过滤器链的末尾调用一下servlet的service方法,拿到返回结果,到此为止,在这一次servlet请求中该Wrapper容器的任务就完成了。

过滤器链(ApplicationFilterChain)的doFilter方法会调用internalDoFilter方法,过滤器链的执行逻辑就在internalDoFilter方法中,为了表达主要意思,我将删减后的代码展示给你,执行后的效果就和上面画的【Servlet过滤器链调用示意图】一样。

private void internalDoFilter(ServletRequest request,ServletResponse response) throws IOException, ServletException {

    // Call the next filter if there is one
    if (pos < n) {
        ApplicationFilterConfig filterConfig = filters[pos++];
        Filter filter = filterConfig.getFilter();
        filter.doFilter(request, response, this);
        return;
    }

    // We fell off the end of the chain -- call the servlet instance
    servlet.service(request, response);
}

OK,StandardWrapper的功能介绍就到此为止,Wrapper容器是对某个具体servlet的一个封装,并提供了 SingleThreadModel和过滤器的机制来丰富 servlet 的功能,通过这篇文章你应该也对过滤器有了一个全面的认识,这下不会再对Tomcat的过滤器和spring的拦截器傻傻分不清楚了吧,如果你真的还不清楚的话,就等我出spring mvc的文章吧,哈哈!

下篇文章来分析下Context容器的标准实现:StandardContext,敬请期待!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/750347.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Vite 动态导入警告问题解决方案

如上图我要实现从后台获取权限菜单并动态导入进行渲染 但由于 vite 暂时不支持这种导入方式 图中也给出了提示 本人也是这么去做了 但并没什么卵用 后来参考了 vite 的 import.meta.glob 这种方式 我在处理菜单权限控制的菜单里进行了如下操作&#xff1a; …

数据转换 | Matlab基于R对称点模式(symmetric dot pattern, SDP)一维序列信号转二维时频图象

目录 效果分析基本介绍程序设计参考资料获取方式 效果分析 基本介绍 数据转换 | Matlab基于R对称点模式(symmetric dot pattern, SDP)一维序列信号转二维时频图象 SDP常被用于信号分析和深度学习模式识别。 SDP是一种基于极坐标系的图像表示方法&#xff0c;可以直接将原始信…

版本控制工具-git的基本使用

目录 前言一、git简介二、git工作流程三、安装git并配置git3.1 配置用户名和邮箱3.2 配置.gitignore文件&#xff08;可选&#xff09;3.3 配置ssh key&#xff08;可选&#xff09; 四、git基本命令4.1 创建本地仓库4.2 将工作区内容提交到本地仓库4.3 将本地仓库内容推送到远…

【例子】webpack 开发一个可以加载 markdown 文件的加载器 loader 案例

Loader 作为 Webpack 的核心机制&#xff0c;内部的工作原理却非常简单。接下来我们一起来开发一个自己的 Loader&#xff0c;通过这个开发过程再来深入了解 Loader 的工作原理。 这里我的需求是开发一个可以加载 markdown 文件的加载器&#xff0c;以便可以在代码中直接导入 m…

nacos漏洞汇总

1 nacos介绍 1.1 nacos是啥 Alibaba Nacos是阿里巴巴推出来的一个新开源项目&#xff0c;是一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。致力于帮助发现、配置和管理微服务。Nacos提供了一组简单易用的特性集&#xff0c;可以快速实现动态服务发现、服…

【数据结构】(C语言):动态数组

动态数组&#xff1a; 内存区域连续&#xff0c;即每个元素的内存地址连续。可用索引查看元素&#xff0c;数组[索引号]。指定位置删除元素&#xff0c;该位置之后的元素全部往前移动一位。指定位置添加元素&#xff0c;从最后到该位置的元素全部往后移动一位。物理大小&#…

量化交易 - 策略回测

策略回测 1、什么是策略回测&#xff1f;2、策略回测的作用3、策略回测系统概述3.1策略回测中相关的指标介绍3.2量化交易策略的资金容量3.3 完整的策略回测系统包含哪些内容 1、什么是策略回测&#xff1f; 策略回测&#xff0c;也称之为策略回溯测试&#xff0c;是指利用交易…

NLP经典论文研读--xlnet论文代码复现记录

xlnet源码解读(简易pytorch实现版本) xlnet这个模型还是相当复杂的&#xff0c;我看了很长一段时间也还是有很多地方没有搞明白&#xff0c;最后又在网上搜了很多大佬写的相关博客&#xff0c;才算是大致弄明白了&#xff0c;想了解xlnet的原理&#xff0c;请参考原论文&#…

微服务框架中的Eureka和Ribbon的个人理解

微服务框架需要学习的东西很多&#xff0c;基本上我把它分为了五个模块&#xff1a; 第一&#xff1a;微服务技术模块 分为三个常用小模块&#xff1a; 1.微服务治理&#xff1a; 注册发现 远程调用 配置管理 网关路由 2.微服务保护&#xff1a; 流量控制 系统保护 熔断降级 服…

【尚庭公寓SpringBoot + Vue 项目实战】移动端登录管理(二十)

【尚庭公寓SpringBoot Vue 项目实战】移动端登录管理&#xff08;二十&#xff09; 文章目录 【尚庭公寓SpringBoot Vue 项目实战】移动端登录管理&#xff08;二十&#xff09;1、登录业务2、接口开发2.1、获取短信验证码2.2、登录和注册接口2.3、查询登录用户的个人信息 1、…

gbase 8c分布式升级步骤

GBase 8c 多模多态企业级分布式数据库具备高性能、高可用、弹性伸缩、高安全性等特性&#xff0c;可以部署在物理机、虚拟机、容器、私有云和公有云&#xff0c;为关键行业核心系统、互联网业务系统和政企业务系统提供安全、稳定、可靠的数据存储和管理服务。GBase 8c支持行存、…

视频号的视频怎么提取文案?详细教程来啦!

很多人不知道视频号的视频怎么提取文案&#xff0c;今天就和大家详细说说视频号视频提取文案的方法&#xff01; 众所众知视频号的视频无法下载&#xff0c;我们该怎么提取视频号的视频呢&#xff1f; 关于下载视频号的视频&#xff0c;首先给大家两种方案&#xff0c;一种就是…

软考在事业单位可以评高级职称的吗?

导言&#xff1a; 我本人计算机专业&#xff0c;就业于一个事业单位&#xff0c;单位有一个副高级职称数&#xff0c;若干个中级职称数&#xff0c;我可不可以通过软考拿到的中级证书&#xff0c;去找领导申请单位聘我的中级职称&#xff0c;高级职称呢&#xff1f; 以下&…

C++之STL(九)

1、函数对象 什么适合推荐使用函数对象&#xff1f; 需要状态的函数调用: 需要状态的函数调用: 函数对象可以包含成员变量&#xff0c;可以在多次调用中保持状态。这在某些算法中非常有用。 提高性能: 编译器可以更好地优化函数对象&#xff0c;因为它们是具体的类型&#xf…

【路由交换技术】Cisco Packet Tracer基础入门教程(四)

Hello各位&#xff0c;好久不见&#xff0c;第四期我准备讲一下Packet Tracer中DHCP的配置&#xff0c;使用方法。 本章实验我们将拓扑中的某个路由器作为DHCP服务器&#xff08;它仍然可作为路由器使用&#xff09;&#xff0c;通过命令配置DHCP服务。独立的服务器可通过图形化…

基于VUE3+VITE+SpringBoot+Nginx部署项目之跨域配置等问题

前言&#xff1a;遇到问题&#xff0c;解决问题。 第一部分&#xff1a;VUE 配置 1、vite.config.js 文件 server: {proxy: {/api: {target: env.VITE_BASE_URL,changeOrigin: true,secure: false,rewrite: path > path.replace(/^\/api/, )}}}, 2、.env 文件 VITE_BAS…

JavaScript算法之龟兔赛跑

简介:龟兔赛跑算法,又称弗洛伊德循环检测算法,是一种在链表中非常常用的算法。它基于运动学和直觉的基本定律。本文旨在向您简要介绍该算法,并帮助您了解这个看似神奇的算法。 假设高速公路上有两辆车。其中一辆的速度为 x,另一辆的速度为 2x。它们唯一能相遇的条件是它们…

UE4 Unlua的快速使用

目录 Unlua的使用前言下载Unlua插件插件安装快速入门语法汇总模块导入多行字符串官方静态方法调用蓝图方法调用重载蓝图中的方法主动调用被重载的蓝图方法输入绑定动态绑定Lua脚本委托容器使用 延迟与协程的使用C 调用Lua 静态导出自定义类型到Lua使用网络UMG资源释放自定义加载…

如何寻找强势货币和弱势货币?

外汇交易的独特之处在于&#xff0c;它融合了两种货币的价值&#xff0c;其中一种货币的价值通过另一种货币来体现。举例来说&#xff0c;USDJPY外汇反映了美元与日元之间的价值关系&#xff0c;而EURUSD则代表了欧元与美元的价值对比。 通过开仓操作&#xff0c;我们预测一种…

继续捡钱,每天几百块!

每日操作计划&#xff1a; 标普信息科技(161128)&#xff0c;溢价8.5%&#xff0c;限购100&#xff0c;一拖七&#xff0c;单户每天700*8.5%59元 印度基金LOF(164824)&#xff0c;溢价2.6%&#xff0c;限购100&#xff0c;一拖七&#xff0c;单户每天700*2.6%18元 美元债LOF(…