栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

SpringBoot中用于解决跨域的@CrossOrigin注解是如何工作的(源码解析)

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

SpringBoot中用于解决跨域的@CrossOrigin注解是如何工作的(源码解析)

注:文中斜体字部分为非正文内容,可略过。


一、引言
    场景:前端项目运行在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方法打个断点,就能找到是谁添加了这些拦截器,最终串起来,就有了这篇博文。

感谢批评指正。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/704218.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号