SpringMVC 源码分析之 FrameworkServlet

前面和小伙伴们聊了 SpringMVC 的初始化流程,相信大家对于 SpringMVC 的初始化过程都有一个基本认知了,今天我们就来看看当一个请求到达后,它的执行流程是什么样的?当然这个流程比较长,松哥这里可能会分两篇文章来和大家分享。

很多小伙伴都知道 SpringMVC 的核心是 DispatcherServlet,而 DispatcherServlet 的父类就是 FrameworkServlet,因此我们先来看看 FrameworkServlet,这有助于我们理解 DispatcherServlet。

1.FrameworkServlet

FrameworkServlet 继承自 HttpServletBean,而 HttpServletBean 继承自 HttpServlet,HttpServlet 就是 JavaEE 里边的东西了,这里我们不做讨论,从 HttpServletBean 开始就是框架的东西了,但是 HttpServletBean 比较特殊,它的特殊在于它没有进行任何的请求处理,只是参与了一些初始化的操作,这些比较简单,而且我们在上篇文章中也已经分析过了,所以这里我们对 HttpServletBean 不做分析,就直接从它的子类 FrameworkServlet 开始看起。

和所有的 Servlet 一样,FrameworkServlet 对请求的处理也是从 service 方法开始,我们先来看看该方法 FrameworkServlet#service:

@Override
protected void service(HttpServletRequest request, HttpServletResponse response)
		throws ServletException, IOException {
	HttpMethod httpMethod = HttpMethod.resolve(request.getMethod());
	if (httpMethod == HttpMethod.PATCH || httpMethod == null) {
		processRequest(request, response);
	}
	else {
		super.service(request, response);
	}
}

可以看到,在该方法中,首先获取到当前请求方法,然后对 patch 请求额外关照了下,其他类型的请求统统都是 super.service 进行处理。

然而在 HttpServlet 中并未对 doGet、doPost 等请求进行实质性处理,所以 FrameworkServlet 中还重写了各种请求对应的方法,如 doDelete、doGet、doOptions、doPost、doPut、doTrace 等,其实就是除了 doHead 之外的其他方法都重写了。

我们先来看看 doDelete、doGet、doPost 以及 doPut 四个方法:

@Override
protected final void doGet(HttpServletRequest request, HttpServletResponse response)
		throws ServletException, IOException {
	processRequest(request, response);
}
@Override
protected final void doPost(HttpServletRequest request, HttpServletResponse response)
		throws ServletException, IOException {
	processRequest(request, response);
}
@Override
protected final void doPut(HttpServletRequest request, HttpServletResponse response)
		throws ServletException, IOException {
	processRequest(request, response);
}
@Override
protected final void doDelete(HttpServletRequest request, HttpServletResponse response)
		throws ServletException, IOException {
	processRequest(request, response);
}

可以看到,这里又把请求交给 processRequest 去处理了,在 processRequest 方法中则会进一步调用到 doService,对不同类型的请求分类处理。

doOptions 和 doTrace 则稍微有些差异,如下:

@Override
protected void doOptions(HttpServletRequest request, HttpServletResponse response)
		throws ServletException, IOException {
	if (this.dispatchOptionsRequest || CorsUtils.isPreFlightRequest(request)) {
		processRequest(request, response);
		if (response.containsHeader("Allow")) {
			return;
		}
	}
	super.doOptions(request, new HttpServletResponseWrapper(response) {
		@Override
		public void setHeader(String name, String value) {
			if ("Allow".equals(name)) {
				value = (StringUtils.hasLength(value) ? value + ", " : "") + HttpMethod.PATCH.name();
			}
			super.setHeader(name, value);
		}
	});
}
@Override
protected void doTrace(HttpServletRequest request, HttpServletResponse response)
		throws ServletException, IOException {
	if (this.dispatchTraceRequest) {
		processRequest(request, response);
		if ("message/http".equals(response.getContentType())) {
			return;
		}
	}
	super.doTrace(request, response);
}

可以看到这两个方法的处理多了一层逻辑,就是去选择是在当前方法中处理对应的请求还是交给父类去处理,由于 dispatchOptionsRequest 和 dispatchTraceRequest 变量默认都是 false,因此默认情况下,这两种类型的请求都是交给了父类去处理。

2.processRequest

我们再来看 processRequest,这算是 FrameworkServlet 的核心方法了:

protected final void processRequest(HttpServletRequest request, HttpServletResponse response)
		throws ServletException, IOException {
	long startTime = System.currentTimeMillis();
	Throwable failureCause = null;
	LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
	LocaleContext localeContext = buildLocaleContext(request);
	RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
	ServletRequestAttributes requestAttributes = buildRequestAttributes(request, response, previousAttributes);
	WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
	asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new RequestBindingInterceptor());
	initContextHolders(request, localeContext, requestAttributes);
	try {
		doService(request, response);
	}
	catch (ServletException | IOException ex) {
		failureCause = ex;
		throw ex;
	}
	catch (Throwable ex) {
		failureCause = ex;
		throw new NestedServletException("Request processing failed", ex);
	}
	finally {
		resetContextHolders(request, previousLocaleContext, previousAttributes);
		if (requestAttributes != null) {
			requestAttributes.requestCompleted();
		}
		logResult(request, response, failureCause, asyncManager);
		publishRequestHandledEvent(request, response, startTime, failureCause);
	}
}

这个方法虽然比较长,但是其实它的核心就是最中间的 doService 方法,以 doService 为界,我们可以将该方法的内容分为三部分:

  1. doService 之前主要是一些准备工作,准备工作主要干了两件事,第一件事就是从 LocaleContextHolder 和 RequestContextHolder 中分别获取它们原来保存的 LocaleContext 和 RequestAttributes 对象存起来,然后分别调用 buildLocaleContext 和 buildRequestAttributes 方法获取到当前请求的 LocaleContext 和 RequestAttributes 对象,再通过 initContextHolders 方法将当前请求的 LocaleContext 和 RequestAttributes 对象分别设置到 LocaleContextHolder 和 RequestContextHolder 对象中;第二件事则是获取到异步管理器并设置拦截器。
  2. 接下来就是 doService 方法,这是一个抽象方法,具体的实现在 DispatcherServlet 中,这个松哥放到 DispatcherServlet 中再和大家分析。
  3. 第三部分就是 finally 中,这个里边干了两件事:第一件事就是将 LocaleContextHolder 和 RequestContextHolder 中对应的对象恢复成原来的样子(参考第一步);第二件事就是通过 publishRequestHandledEvent 方法发布一个 ServletRequestHandledEvent 类型的消息。

经过上面的分析,大家发现,processRequest 其实主要做了两件事,第一件事就是对 LocaleContext 和 RequestAttributes 的处理,第二件事就是发布事件。我们对这两件事分别来研究。

2.1 LocaleContext 和 RequestAttributes

LocaleContext 和 RequestAttributes 都是接口,不同的是里边存放的对象不同。

2.1.1 LocaleContext

LocaleContext 里边存放着 Locale,也就是本地化信息,如果我们需要支持国际化,就会用到 Locale。

国际化的时候,如果我们需要用到 Locale 对象,第一反应就是从 HttpServletRequest 中获取,像下面这样:

Locale locale = req.getLocale();

但是大家知道,HttpServletRequest 只存在于 Controller 中,如果我们想要在 Service 层获取 HttpServletRequest,就得从 Controller 中传参数过来,这样就比较麻烦,特别是有的时候 Service 中相关方法都已经定义好了再去修改,就更头大了。

所以 SpringMVC 中还给我们提供了 LocaleContextHolder,这个工具就是用来保存当前请求的 LocaleContext 的。当大家看到 LocaleContextHolder 时不知道有没有觉得眼熟,松哥在之前的 Spring Security 系列教程中和大家聊过 SecurityContextHolder,这两个的原理基本一致,都是基于 ThreadLocal 来保存变量,进而确保不同线程之间互不干扰,对 ThreadLocal 不熟悉的小伙伴,可以看看松哥的 Spring Security 系列,之前有详细分析过(公号后台回复 ss)。

有了 LocaleContextHolder 之后,我们就可以在任何地方获取 Locale 了,例如在 Service 中我们可以通过如下方式获取 Locale:

Locale locale = LocaleContextHolder.getLocale();

上面这个 Locale 对象实际上就是从 LocaleContextHolder 中的 LocaleContext 里边取出来的。

需要注意的是,SpringMVC 中还有一个 LocaleResolver 解析器,所以前面 req.getLocale() 并不总是获取到 Locale 的值,这个松哥在以后的文章中再和小伙伴们细聊。

2.1.2 RequestAttributes

RequestAttributes 是一个接口,这个接口可以用来 get/set/remove 某一个属性。

RequestAttributes 有诸多实现类,默认使用的是 ServletRequestAttributes,通过 ServletRequestAttributes,我们可以 getRequest、getResponse 以及 getSession。

在 ServletRequestAttributes 的具体实现中,会通过 scope 参数判断操作 request 还是操作 session(如果小伙伴们不记得 Spring 中的作用域问题,可以公号后台回复 spring,看看松哥录制的免费的 Spring 入门教程,里边有讲),我们来看一下 ServletRequestAttributes#setAttribute 方法(get/remove 方法执行逻辑类似):

public void setAttribute(String name, Object value, int scope) {
    if (scope == 0) {
        if (!this.isRequestActive()) {
            throw new IllegalStateException("Cannot set request attribute - request is not active anymore!");
        }
        this.request.setAttribute(name, value);
    } else {
        HttpSession session = this.obtainSession();
        this.sessionAttributesToUpdate.remove(name);
        session.setAttribute(name, value);
    }
}

可以看到,这里会先判断 scope,scope 为 0 就操作 request,scope 为 1 就操作 session。如果操作的是 request,则需要首先通过 isRequestActive 方法判断当前 request 是否执行完毕,如果执行完毕,就不可以再对其进行其他操作了(当执行了 finally 代码块中的 requestAttributes.requestCompleted 方法后,isRequestActive 就会返回 false)。

和 LocaleContext 类似,RequestAttributes 被保存在 RequestContextHolder 中,RequestContextHolder 的原理也和 SecurityContextHolder 类似,这里不再赘述。

看了上面的讲解,大家应该发现了,在 SpringMVC 中,如果我们需要在 Controller 之外的其他地方使用 request、response 以及 session,其实不用每次都从 Controller 中传递 request、response 以及 session 等对象,我们完全可以直接通过 RequestContextHolder 来获取,像下面这样:

ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = servletRequestAttributes.getRequest();
HttpServletResponse response = servletRequestAttributes.getResponse();

是不是非常 easy!

2.2 事件发布

最后就是 processRequest 方法中的事件发布了。

在 finally 代码块中会调用 publishRequestHandledEvent 方法发送一个 ServletRequestHandledEvent 类型的事件,具体发送代码如下:

private void publishRequestHandledEvent(HttpServletRequest request, HttpServletResponse response,
		long startTime, @Nullable Throwable failureCause) {
	if (this.publishEvents && this.webApplicationContext != null) {
		// Whether or not we succeeded, publish an event.
		long processingTime = System.currentTimeMillis() - startTime;
		this.webApplicationContext.publishEvent(
				new ServletRequestHandledEvent(this,
						request.getRequestURI(), request.getRemoteAddr(),
						request.getMethod(), getServletConfig().getServletName(),
						WebUtils.getSessionId(request), getUsernameForRequest(request),
						processingTime, failureCause, response.getStatus()));
	}
}

可以看到,事件的发送需要 publishEvents 为 true,而该变量默认就是 true。如果需要修改该变量的值,可以在 web.xml 中配置 DispatcherServlet 时,通过 init-param 节点顺便配置一下该变量的值。正常情况下,这个事件总是会被发送出去,如果项目有需要,我们可以监听该事件,如下:

@Component
public class ServletRequestHandleListener implements ApplicationListener<ServletRequestHandledEvent> {
    @Override
    public void onApplicationEvent(ServletRequestHandledEvent servletRequestHandledEvent) {
        System.out.println("请求执行完毕-"+servletRequestHandledEvent.getRequestUrl());
    }
}

当一个请求执行完毕时,该事件就会被触发。

3.小结

这篇文章主要和小伙伴们分享了 SpringMVC 中 DispatcherServlet 的父类 FrameworkServlet,FrameworkServlet 的功能其实比较简单,主要就是在 service 方法中增加了对 PATCH 的处理,然后其他类型的请求都被归类到 processRequest 方法中进行统一处理,processRequest 方法则又分了三部分,首先是对 LocaleContext 和 RequestAttributes 的处理,然后执行 doService,最后在 finally 代码块中对 LocaleContext 和 RequestAttributes 属性进行复原,同时发布一个请求结束的事件。

doService 是重头戏,松哥将在下篇文章中和大家分享。好啦,今天就先和小伙伴们聊这么多~

原文地址:https://blog.csdn.net/u012702547/article/details/115108949

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


开发过程中是不可避免地会出现各种异常情况的,例如网络连接异常、数据格式异常、空指针异常等等。异常的出现可能导致程序的运行出现问题,甚至直接导致程序崩溃。因此,在开发过程中,合理处理异常、避免异常产生、以及对异常进行有效的调试是非常重要的。 对于异常的处理,一般分为两种方式: 编程式异常处理:是指在代
说明:使用注解方式实现AOP切面。 什么是AOP? 面向切面编程,利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。 通俗描述:不通过修改源代码方式,在主干功能里面添加新功能。 AOP底层使用动态代理。 AOP术语 连接点
Spring MVC中的拦截器是一种可以在请求处理过程中对请求进行拦截和处理的机制。 拦截器可以用于执行一些公共的操作,例如日志记录、权限验证、数据转换等。在Spring MVC中,可以通过实现HandlerInterceptor接口来创建自定义的拦截器,并通过配置来指定拦截器的应用范围和顺序。 S
在 JavaWeb 中,共享域指的是在 Servlet 中存储数据,以便在同一 Web 应用程序的多个组件中进行共享和访问。常见的共享域有四种:ServletContext、HttpSession、HttpServletRequest、PageContext。 ServletContext 共享域:
文件上传 说明: 使用maven构建web工程。 使用Thymeleaf技术进行服务器页面渲染。 使用ResponseEntity实现下载文件的功能。 @Controller public class FileDownloadAndUpload { @GetMapping(&quot;/file/d
创建初始化类,替换web.xml 在Servlet3.0环境中,Web容器(Tomcat)会在类路径中查找实现javax.servlet.ServletContainerInitializer接口的类,如果找到的话就用它来配置Servlet容器。 Spring提供了这个接口的实现,名为SpringS
在 Web 应用的三层架构中,确保在表述层(Presentation Layer)对数据进行检查和校验是非常重要的。正确的数据校验可以确保业务逻辑层(Business Logic Layer)基于有效和合法的数据进行处理,同时将错误的数据隔离在业务逻辑层之外。这有助于提高系统的健壮性、安全性和可维护
什么是事务? 事务(Transaction)是数据库操作最基本单元,逻辑上一组操作,要么都成功,要么都失败,如果操作之间有一个失败所有操作都失败 。 事务四个特性(ACID) 原子性 一组操作要么都成功,要么都失败。 一致性 一组数据从事务1合法状态转为事务2的另一种合法状态,就是一致。 隔离性 事
什么是JdbcTemplate? Spring 框架对 JDBC 进行封装,使用 JdbcTemplate 方便实现对数据库操作。 准备工作 引入jdbcTemplate的相关依赖: 案例实操 创建jdbc.properties文件,配置数据库信息 jdbc.driver=com.mysql.cj.
SpringMVC1.MVC架构MVC是模型(Model)、视图(View)、控制器(Controller)的简写,是一种软件设计规范是将业务逻辑、数据、显示分离的方法来写代码MVC主要作用是:降低了视图和业务逻辑之间的双向耦合MVC是一个架构模型,不是一种设计模式。1.model(模型)数据模型,提供要展示的数据,因此包
SpringMVC学习笔记1.SpringMVC应用1.1SpringMVC简介​SpringMVC全名叫SpringWebMVC,是⼀种基于Java的实现MVC设计模型的请求驱动类型的轻量级Web框架,属于SpringFrameWork的后续产品。​MVC全名是ModelViewController,是模型(model)-视图(view)-控制器(co
11.1数据回显基本用法数据回显就是当用户数据提交失败时,自动填充好已经输入的数据。一般来说,如果使用Ajax来做数据提交,基本上是没有数据回显这个需求的,但是如果是通过表单做数据提交,那么数据回显就非常有必要了。11.1.1简单数据类型简单数据类型,实际上框架在这里没有
一、SpringMVC简介1、SpringMVC中重要组件DispatcherServlet:前端控制器,接收所有请求(如果配置/不包含jsp)HandlerMapping:解析请求格式的.判断希望要执行哪个具体的方法.HandlerAdapter:负责调用具体的方法.ViewResovler:视图解析器.解析结果,准备跳转到具体的物
1.它们主要负责的模块Spring主要应用于业务逻辑层。SpringMVC主要应用于表现层。MyBatis主要应用于持久层。2.它们的核心Spring有三大核心,分别是IOC(控制反转),DI(依赖注入)和AOP(面向切面编程)。SpringMVC的核心是DispatcherServlet(前端控制器)。MyBatis的核心是ORM(对
3.注解开发Springmvc1.使用注解开发要注意开启注解支持,2.注解简化了,处理映射器和处理适配器,只用去管视图解析器即可案例代码:1.web.xml,基本不变可以直接拿去用<!--调用DispatcherServlet--><servlet><servlet-name>springmvc</servlet-name>
拦截器概述SpringMVC的处理器拦截器类似于Servlet开发中的过滤器Filter,用于对处理器进行预处理和后处理。开发者可以自己定义一些拦截器来实现特定的功能。**过滤器与拦截器的区别:**拦截器是AOP思想的具体应用。过滤器servlet规范中的一部分,任何javaweb工程都可以使用
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:context="http://www.springframework.org/schema/context"xmlns:mvc="http://www.springframework.org/schema/mvc"xmlns:xsi="
学习内容:1、SSH&SSM2、Spring3、Struts2&SpringMVC4、Hibernate&MyBatis学习产出:1.SSH和SSM都是有Spring框架的,他们两个差不多。2.Spring分为四个模块,持久层,表示层,检测层,还有核心层,核心层分为2个关键核心功能。分别为,控制反转(IOC),依赖注入(DI),和面向切面编程
一、SpringMVC项目无法引入js,css的问题具体原因是css和js等被SpringMVC拦截了:解决方案:在spring-mvc.xml中配置<mvc:default-servlet-handler/><?xmlversion="1.0"encoding="UTF-8"?><beansxmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
开发环境:Eclipse/MyEclipse、Tomcat8、Jdk1.8数据库:MySQL前端:JavaScript、jQuery、bootstrap4、particles.js后端:maven、SpringMVC、MyBatis、ajax、mysql读写分离、mybatis分页适用于:课程设计,毕业设计,学习等等系统介绍