注:文中斜体字部分为非正文内容,可略过。
一、引言
- 场景:前端项目运行在8081端口,在页面内向8080端口的SpringBoot服务请求数据,这属于“跨域”情形。此时,我们需要在SpringBoot中配置响应头(等),否则浏览器会禁止前端页面获取响应数据。
- 最简单的处理方式是:在Controller类或其方法上直接加
@CrossOrigin注解,跨域问题就可以被悄无声息地解决。在好奇心的驱使下,我决定看一下它在源码中的实现原理。
二、结论
江湖规矩:先说结论,再谈细节。
不必说,SpringBoot(MVC)的核心必然围绕着DispatcherServlet展开,所以我们直接进入它的doDispatch()方法来说明结论——
- 第一阶段:如下图所示,在
getHandler()方法内部,SpringBoot(MVC)为我们在底层添加了一个类型为CorsInterceptor的拦截器对象。 - 第二阶段:
applyPreHandle()内部,调用了该CorsInterceptor对象的preHandle()方法,以完成对响应头属性"headers"的配置,从而解决跨域问题。 - 所以:当我们使用
@CrossOrigin注解时,程序在底层添加了一个拦截器来修改response的headers属性,从而解决跨域问题。

那么让我们进入这两个断点的内部一探究竟。
三、解析源码(按代码执行顺序)
首先,“展示”一下我的Controller。乏善可陈,只是加了一个@CrossOrigin注解。

- 接下来开始debug:
在前端页面触发请求,请求路径为localhost:8080/test。我们直接进入(第一阶段)第一个断点内部:

此时,SpringBoot遍历handlerMappings并调用getHandler()方法(集合中第一个RequestMappingHandlerMapping就是专门用于处理“标注了@RequestMapping注解的方法”的,不做详解)我们直接step into这个方法内部。
接下来我们看见的代码如下,注意图中文字标注,“不重要区域”的代码不涉及跨域处理(有些童鞋可能对这段已经熟悉了),所以我们直接关注重点代码:

首先是重要区域的第一行,if语句判断逻辑的左半边。step into,该方法内部如下图所示:

继续进入第一行判断,看到下图代码。(此时我并不熟悉CorsConfigurationSource,但通过注释以及查看它的源码可知:这是一个能够被我们的Controller实现的接口,其目的是提供一个CorsCongiguration对象,该对象似乎能够为我们的Controller针对性地配置拦截行为,而不是采取默认行为。值得一提的是:我们的拦截器CorsInterceptor实现了这个CorsConfigurationSource接口)此方法最终返回false:

这里返回之后,继续回到刚才的判断(如图),进去瞅一眼。

内部代码如下图所示,(里面的第一行代码有些怪异 —— 调用一个HandlerMethod对象的方法仍返回一个HandlerMethod对象。step into 发现返回值是该对象的resolvedFromHandlerMethod属性(在构造器中初始化),而这个HandlerMethod对象是作为参数从外面一路传入此处的,它的诞生显然至少要追溯到SpringBoot构建HandlerMapping映射的时候(路径&方法映射),属于启动过程的范畴,这里不再探究。)
第二行,corsLookup是一个ConcurrentHashMap,此处以original为键,来获取其对应的值,即CorsConfiguration对象。(至于这个Map是啥时候出现的,其实是SpringBoot启动过程中为handler方法注册映射的时候(register方法),遍历了beanType和method的接口,如果找到@CrossOrign类型的注解,就向这个Map中put一个 该handler和corsConfig 的映射,此处就不详述了,我们专注于当前正在执行的代码。)

至此,就做完了前文“重要区域”的第一行判断。第二行是获取config的方法,追进去之后仍然是类似代码,唯一不同之处就是判断了是否需要添加额外的CorsConfiguration信息,不做深入研究。我们加快一下脚步,下面来到第一阶段最关键的一行代码:

step into这个方法,如下图所示,我们的核心选手CorsInterceptor,也就是上文结论中提到的Interceptor终于亮相了,并且是直接new出来的:

它的结构如下图,先混个眼熟,一会儿还要见面。

从它的构造器出来,我们续进入chain.addInterceptor()方法,非常简单,就是向interceptorList中添加我们传入的interceptor(可以看到interceptorList中此前已经拥有了两个元素,它们是在前文的“不重要区域”被添加到该集合中的)

这行代码执行完,CorsInterceptor就已经被成功添加到执行链当中,且指定了下标为0,如图所示。至此,第一阶段(添加拦截器)圆满成功。

下面我们马不停蹄,一路向外,直到返回SpringMVC的中央枢纽——DispatcherServlet,随后进入第二阶段——执行拦截器。

接下来其实就很简单了,无需赘述,所以直接深入到CorsInterceptor的preHandle()方法,如图。

step into processRequest()方法,首先获取一个varyHeaders(这里并不重要,但我有些好奇看了看,发现使用了response对象的一个"非法偷渡"的属性(coyoteResponse)来获取一个枚举对象,从而获取几个字符串的List,没有继续深究),不做赘述。

继续向下,进入第二阶段的关键方法:

进来之后,如图简要分析一下,这个“千呼万唤始出来”的方法,里面的代码就非常简单了——设置了Access-Control-Allow-Origin和*的响应头键值对。至此,跨域问题解决,程序继续执行其他拦截器,以及常规的MVC流程。

补充:这里我留意到了一个“小插曲”,如下图,这个allowMethods只有三个请求方式,也就是说如果以后用Restful风格,发送DELETE或者PUT请求或许会出现跨域失败的问题(在config为默认时),此处暂时留个印象,等以后遇到了再研究,不过我估计使用CorsConfiguration定义一下allowMethods属性就可以了。

四、逆序追踪源码的过程(简述)
在我试图寻找@CrossOrigin源码的原理时,是没有办法在一开始就准确地找到DispatcherServlet中的入口的。所以,实际分析时,并不能按照上面的顺序一步步debug,只能反向追根溯源。下面简单记录一下实际过程。
- 最开始没有头绪,需要寻找切入点。既然我们研究的是跨域,它的关键就在response对象的heads属性上,于是乎,我需要找到headers被修改的地方,就能找到是谁在幕后操纵一切。
- 所以我仍然先从
DispatcherServlet中入手,来查看response是哪个具体实现类的对象,可以很容易发现两个类:ResponseFacade和ServletServerHttpResponse。于是,我在它们身上,把凡是涉及到headers属性的set()、add、write方法全部打上断点。 - 重新debug,果然发现了有人在修改headers属性,然后从修改发生处,不断跳出调用栈,反查调用者,这一路跳出,就遇到了拦截器的
preHandle()方法,一看到拦截器,瞬间就明白了。 - 接下来的事儿就好办了,给拦截器列表
inteceptorList的add方法打个断点,就能找到是谁添加了这些拦截器,最终串起来,就有了这篇博文。
感谢批评指正。
SpringBoot @CrossOrigin注解源码解析:解决跨域原理
本文详细介绍了SpringBoot中@CrossOrigin注解的工作原理,通过源码分析揭示了其如何通过添加拦截器修改响应头以解决跨域问题。在Controller上使用该注解时,程序会在底层创建一个CorsInterceptor实例,通过处理响应头来实现跨域支持。
163

被折叠的 条评论
为什么被折叠?



