首页 文章 精选 留言 我的

精选列表

搜索[智博会],共10000篇文章
优秀的个人博客,低调大师

每日一博 | 如何根治 Script Error

作者:卢峰(清锐) 本文简要介绍了 Script Error 问题的来龙去脉,但也不局限于 Script Error,对于通用的系统性问题,应该找到系统性解决方案,进而治标治本。 Script Error 原因与当前解法 受浏览器同源策略限制,未知跨域脚本执行错误时,抛出的错误信息为 "Script error.",导致开发者无法定位具体错误。为了获取详细错误信息及堆栈,一般解法是给 Script 标签配置 crossorigin 属性,同时对应脚本服务端需配置 Access-Control-Allow-Origin 响应头。 另外还有一些 hack 解法,对浏览器原生 API 做代理,将业务代码放在 Try Catch 作用域中执行,但写好代理方法是不容易的,粗制滥造的代理方法会制造很多隐藏 Bug,并且大量 Try Catch 在一些 JS Engine 中也存在额外性能损耗,为了解决 Script Error 采用此方案得不偿失。 还有什么问题 crossorigin 不好加 异步加载脚本套娃,A 加载 B,B 加载 C,以至于不知道加载了哪些外部脚本 需要服务端配合设置响应头 Access-Control-Allow-Origin crossorigin 加不了 外部注入代码,如浏览器插件、定制 Webview 容器(xx 浏览器) 无效 Script Error 数据,难以评估对业务实际影响,并且耗费监控资源 溯源:为什么是 Script Error 从 2006 年一篇安全漏洞文章说起:I know if you're logged-in, anywhere 在那个年代大量网站都是服务端渲染,服务端根据用户登录态返回不同页面内容,黑客通过 Script 加载目标站点,用户已登录、未登录返回的 Response 内容不同,报错信息也会有差异,这样就可以通过报错信息区分用户是否登录,进一步展开针对性的攻击。 <script src=” http://mail.google.com/mail/”></script> 已登录: **未登录: ** 对于其他站点也是类似,错误信息中总会有差异,比如亚马逊登录和未登录,报错的 LineNo 不同。 基于此,WHATWG 对错误信息透出制定了规范: Chrome 实现: 《I know if you're logged-in, anywhere》地址:https://blog.jeremiahgrossman.com/2006/12/i-know-if-youre-logged-in-anywhere.html Script Error 规范是否能调整 通过以上信息,我们可以理解 Script Error 的设计初衷以及其合理性,但我也有疑问,在今天浏览器同源策略比较完善的情况下,是否有必要屏蔽所有信息(error message、lineno、colno、url)?能否将发生 Script Error 的脚本 url 暴露出来,以便开发者收集到错误信息时快速定位错误来源,这样也方便评估影响面,比如明显是注入的脚本错误,直接忽略即可。 翻阅 WHATWG Github 历史 issue,发现已经有过相关讨论,很明确答案是 No,大概原因是当前的同源策略已经很全面(复杂),不想在挖坑。以至于对 unhanlderejection,连 Script Error 都不愿意报。 相关讨论地址:https://github.com/whatwg/html/issues/2440 <br><br> unhanlderejection地址:https://github.com/whatwg/html/issues/5051 其他大厂如何处理 Script Error 我在几个大厂网站上做了测试,加载一个第三方脚本,第三方脚本一定会报错,看看对应站点如何处理。 var s = document.createElement('script'); s.src = 'https://g.alicdn.com/dinamic/h5-tb-cart/5.0.41/index.min.js'; document.body.appendChild(s); Google:常规处理,直接上报 Script Error Twitter: 通过 CSP 策略拦截了未知脚本加载,包括 Github、FaceBook 都采用类似方案 QQ 视频:除了上报 Script Error,并监控上报异步加载的脚本 面向未来我们应该如何处理 Script Error 面向未来看问题,我们不能与标准背道而驰,同源策略是当前解决 Web 安全问题的重要手段,在未来只会更完善,我们应该积极了解与应用。当前国内互联网对同源策略的了解与应用大多止步于 Access-Control-Allow-Origin: *,这是远远不够的。 因此,面向未来 Script Error 问题 Twitter 的处理方式相对合理,只允许站点加载白名单脚本,对白名单脚本逐个做 CrossOring 等配置,同时也杜绝了外部脚本注入。对于淘宝来说,受限于业务体量以及历史包袱,做这种改造难度可想而知,但我们应该朝这个方向努力,而不是让开发者面对 Script Error 手足无措,靠猜测或是加错误过滤解决问题。 回到当下,短期的解决方案要增强跨域脚本的感知能力,可以配置 CSP Report Only 上报跨域脚本,也可以通过原始手段统计,进而对相关脚本做跨域配置,对于明显的跨域脚本如埋点、唤端、以及安全系列脚本,缺少 crossorigin 的尽快修复。 CSP Report Only地址:https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only document.querySelectorAll('script[src]:not([crossorigin])') 本文简要介绍了 Script Error 问题的来龙去脉,但也不局限于 Script Error,对于通用的系统性问题,应该找到系统性解决方案,进而治标治本。 参考文档 what is script error(地址:https://blog.sentry.io/2016/05/17/what-is-script-error) Cryptic "Script Error." reported in Javascript in Chrome and Firefox(地址:https://stackoverflow.com/questions/5913978/cryptic-script-error-reported-in-javascript-in-chrome-and-firefox) 解决 "Script Error" 的另类思路(地址:https://juejin.cn/post/6844903727820718094#heading-6) iOS Privacy: Instagram and Facebook can track anything you do on any website in their in-app browser(地址:https://krausefx.com/blog/ios-privacy-instagram-and-facebook-can-track-anything-you-do-on-any-website-in-their-in-app-browser) I know if you're logged-in, anywhere(地址:https://blog.jeremiahgrossman.com/2006/12/i-know-if-youre-logged-in-anywhere.html) HTML Spec: Runtime script errors(地址:https://html.spec.whatwg.org/multipage/webappapis.html#runtime-script-errors) "Script error." message in window.onerror makes bad DevExp trade off(地址:https://github.com/whatwg/html/issues/2440) unhandledrejection should fire even for muted scripts(地址:https://github.com/whatwg/html/issues/5051) Content-Security-Policy-Report-Only(地址:https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only)

优秀的个人博客,低调大师

每日一博 | 实现 LRU 缓存算法

1 LRU 缓存介绍 LRU 算法全称是最近最少使用算法(Least Recently Use),是一种简单的缓存策略。顾名思义,LRU 算法会选出最近最少使用的数据进行淘汰。 那么什么是缓存呢?缓存专业点可以叫一种提高数据读取性能的技术,可以有效解决存储器性能和容量的矛盾,是一种空间换时间的设计思想,比如我们常见的内存是硬盘的缓存,Cache 是内存的缓存,浏览器本地存储是网络访问的缓存...... LRU 有许多应用场景,例如: 操作系统底层的内存管理。 缓存服务,例如 Redis,当数据满的时候就要淘汰掉长期不使用的 key,在 Redis 中用了一个类似的 LRU 算法,而不是严格的 LRU 算法。 MySQL 的 Buffer Pool,也就是缓冲池,它的目的是为了减少磁盘 IO。它是一块连续的内存,当 Buffer Pool 满的时候就要淘汰很久没有被访问过的页。 2 Leetcode 真题 146. LRU 缓存 [1],请你设计并实现一个满足 LRU (最近最少使用) 缓存约束的数据结构。 实现 LRUCache 类: LRUCache(int capacity) 以正整数作为容量capacity 初始化 LRU 缓存。 int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。 void put(int key, int value)如果关键字key 已经存在,则变更其数据值value ;如果不存在,则向缓存中插入该组key-value 。如果插入操作导致关键字数量超过capacity ,则应该逐出最久未使用的关键字。 函数 get 和 put 必须以 O(1) 的平均时间复杂度运行。 示例: 输入 ["LRUCache", "put", "put", "get", "put", "get", "put", "get", "get", "get"] [[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]] 输出 [null, null, null, 1, null, -1, null, -1, 3, 4] 解释 LRUCache lRUCache = new LRUCache(2); lRUCache.put(1, 1); // 缓存是 {1=1} lRUCache.put(2, 2); // 缓存是 {1=1, 2=2} lRUCache.get(1); // 返回 1 lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3} lRUCache.get(2); // 返回 -1 (未找到) lRUCache.put(4, 4); // 该操作会使得关键字 1 作废,缓存是 {4=4, 3=3} lRUCache.get(1); // 返回 -1 (未找到) lRUCache.get(3); // 返回 3 lRUCache.get(4); // 返回 4 3 题目分析 1.首先,题目中提到函数 get 和 put 必须以 O(1) 的平均时间复杂度运行,很自然地我们可以想到应该使用哈希表。 2.其次,当访问数据结构中的某个 key 时,需要将这个 key 更新为最近使用;另外如果 capacity 已满,需要删除访问时间最早的那条数据。这要求数据是有序的,并且可以支持在任意位置快速插入和删除元素,链表可以满足这个要求。 3.结合 1,2 两点来看,我们可以采用哈希表 + 链表的结构实现 LRU 缓存。 如上图所示,就是哈希表 + 链表实现的 LRU 缓存数据结构,有以下几个问题解释一下: 1.为什么这里要使用双向链表,而不是单向链表? 我们在找到了节点,需要删除节点的时候,如果使用单向链表的话,后驱节点的指针是直接能拿到的,但是这里要求时间复杂度是 O(1),要能够直接获取到前驱节点的指针,那么只能使用双向链表。 2.哈希表里面已经保存了 key ,那么链表中为什么还要存储 key 和 value 呢,只存入 value 不就行了? 当我们删除节点的时候,除了需要删除链表中的节点,还需要删除哈希表中的节点。删除哈希表中的节点需要知道 key,所以在链表的节点中需要存储 key 和 value,当删除链表节点时拿到 key,再根据 key 到哈希表中删除节点。 3.虚拟头节点和虚拟尾节点有什么用? 虚拟节点在链表中被广泛应用,又称为哨兵节点,通常不保存任何数据。使用虚拟节点我们可以统一处理链表中所有节点的插入删除操作,而不用考虑头尾两个节点的特殊情况。 4 代码实现 4.1 Golang package main import "fmt" // LRU 数据结构 type LRUCache struct { capacity int // 容量 size int // 已使用空间 head, tail *DLinkedNode // 头节点,尾节点 cache map[int]*DLinkedNode // 哈希表 } // 双向链表数据结构 type DLinkedNode struct { key, value int prev, next *DLinkedNode // 前指针,后指针 } // 创建一个新的节点 func initDLinkedNode(key, value int) *DLinkedNode { return &DLinkedNode{ key: key, value: value, } } // 初始化 LRU 结构 func Constructor(capacity int) LRUCache { l := LRUCache{ cache: map[int]*DLinkedNode{}, // 哈希表 head: initDLinkedNode(0, 0), // 虚拟头节点 tail: initDLinkedNode(0, 0), // 虚拟尾节点 capacity: capacity, // 容量 } // 虚拟头节点和虚拟尾节点互连 l.head.next = l.tail l.tail.prev = l.head return l } // 获取元素 func (this *LRUCache) Get(key int) int { // 如果没有在哈希表中找到 key if _, ok := this.cache[key]; !ok { return -1 } // 如果 key 存在,先通过哈希表定位,再移到头部 node := this.cache[key] this.moveToHead(node) return node.value } // 插入元素 func (this *LRUCache) Put(key int, value int) { // 先去哈希表中查询 // 如果 key 不存在,创建一个新的节点 if node, ok := this.cache[key]; !ok { newNode := initDLinkedNode(key, value) // 如果达到容量限制,链表删除尾部节点,哈希表删除元素 this.size++ if this.size > this.capacity { // 得到删除的节点 removed := this.removeTail() // 根据得到的 key 删除哈希表中的元素 delete(this.cache, removed.key) // 减少已使用容量 this.size-- } // 插入哈希表 this.cache[key] = newNode // 插入链表 this.addToHead(newNode) } else { // 如果 key 存在,先通过哈希表定位,再修改 value,并移到头部 node.value = value this.moveToHead(node) } } // 将节点添加到头部 func (this *LRUCache) addToHead(node *DLinkedNode) { // 新节点指向前后节点 node.prev = this.head node.next = this.head.next // 前后节点指向新节点 this.head.next.prev = node this.head.next = node } // 删除该节点 func (this *LRUCache) removeNode(node *DLinkedNode) { // 修改该节点前后节点的指针,不再指向该节点 node.next.prev = node.prev node.prev.next = node.next } // 移动到头部,也就是当前位置删除,再添加到头部 func (this *LRUCache) moveToHead(node *DLinkedNode) { this.removeNode(node) this.addToHead(node) } // 移除尾部节点,淘汰最久未使用的 func (this *LRUCache) removeTail() *DLinkedNode { node := this.tail.prev // 虚拟尾节点的上一个才是真正的尾节点 this.removeNode(node) return node } // 打印链表(解题不需要此方法,只是为了显示效果) func (this *LRUCache) printDLinkedNode() { p := this.head for p != nil { fmt.Printf("key: %d, value: %d\n", p.key, p.value) p = p.next } } func main() { lru := Constructor(3) fmt.Println("=========================== 插入 3 个节点 ===========================") lru.Put(1, 100) lru.Put(2, 200) lru.Put(3, 300) fmt.Println("=========================== 打印当前链表 ===========================") lru.printDLinkedNode() fmt.Println("=========================== 插入第 4 个节点,LRU 缓存淘汰尾部节点 ===========================") lru.Put(4, 400) lru.printDLinkedNode() fmt.Println("=========================== 获取 key2 节点,更新 LRU 缓存,将会移动至链表头部 ===========================") lru.Get(2) lru.printDLinkedNode() } 4.2 Java import java.util.HashMap; import java.util.Map; public class LRUCache { // 双向链表 class DLinkedNode { int key; int value; DLinkedNode prev; DLinkedNode next; public DLinkedNode() { } public DLinkedNode(int key, int value) { this.key = key; this.value = value; } } // 哈希表 private Map<Integer, DLinkedNode> cache = new HashMap<>(); // 已使用空间 private int size; // 容量 private int capacity; // 头节点,尾节点 private DLinkedNode head, tail; public LRUCache(int capacity) { this.size = 0; this.capacity = capacity; // 使用虚拟头部和虚拟尾部节点 head = new DLinkedNode(); tail = new DLinkedNode(); // 虚拟头节点和虚拟尾节点互连 head.next = tail; tail.prev = head; } // 获取元素 public int get(int key) { DLinkedNode node = cache.get(key); // 如果没有在哈希表中找到 key if (node == null) { return -1; } // 如果 key 存在,先通过哈希表定位,再移到头部 moveToHead(node); return node.value; } // 插入元素 public void put(int key, int value) { DLinkedNode node = cache.get(key); if (node == null) { // 如果 key 不存在,创建一个新的节点 DLinkedNode newNode = new DLinkedNode(key, value); // 如果达到容量限制,链表删除尾部节点,哈希表删除元素 size++; if (size > capacity) { // 得到删除的节点 DLinkedNode removed = removeTail(); // 根据得到的 key 删除哈希表中的元素 cache.remove(removed.key); // 减少已使用容量 size--; } // 插入哈希表 cache.put(key, newNode); // 添加至双链表的头部 addToHead(newNode); } else { // 如果 key 存在,先通过哈希表定位,再修改 value,并移到头部 node.value = value; moveToHead(node); } } // 将节点添加到链表头部 private void addToHead(DLinkedNode node) { // 新节点指向前后节点 node.prev = head; node.next = head.next; // 前后节点指向新节点 head.next.prev = node; head.next = node; } // 删除节点 private void removeNode(DLinkedNode node) { // 修改该节点前后节点的指针,不再指向该节点 node.prev.next = node.next; node.next.prev = node.prev; } // 移动到头部,也就是当前位置删除,再添加到头部 private void moveToHead(DLinkedNode node) { removeNode(node); addToHead(node); } // 移除尾部节点,淘汰最久未使用的 private DLinkedNode removeTail() { DLinkedNode res = tail.prev; // 虚拟尾节点,prev 才是此时真正的尾节点 removeNode(res); return res; } // 打印链表(解题不需要此方法,只是为了显示效果) private void printDLinkedNode() { DLinkedNode p = this.head; while (p != null) { System.out.printf("key: %d, value: %d\n", p.key, p.value); p = p.next; } } public static void main(String[] args) { LRUCache lru = new LRUCache(3); System.out.println("=========================== 插入 3 个节点 ==========================="); lru.put(1, 100); lru.put(2, 200); lru.put(3, 300); System.out.println("=========================== 打印当前链表 ==========================="); lru.printDLinkedNode(); System.out.println("=========================== 插入第 4 个节点,LRU 缓存淘汰尾部节点 key1 ==========================="); lru.put(4, 400); lru.printDLinkedNode(); System.out.println("=========================== 获取 key2 的节点,更新 LRU 缓存,将会移动至链表头部 ==========================="); lru.get(2); lru.printDLinkedNode(); } } 4.3 运行结果 代码运行的返回结果如下,其中头尾两个 key=0, value=0 的节点是虚拟节点,请忽略。 =========================== 插入 3 个节点 =========================== =========================== 打印当前链表 =========================== key: 0, value: 0 key: 3, value: 300 key: 2, value: 200 key: 1, value: 100 key: 0, value: 0 =========================== 插入第 4 个节点,LRU 缓存淘汰尾部节点 =========================== key: 0, value: 0 key: 4, value: 400 key: 3, value: 300 key: 2, value: 200 key: 0, value: 0 =========================== 获取 key2 节点,更新 LRU 缓存,将会移动至链表头部 =========================== key: 0, value: 0 key: 2, value: 200 key: 4, value: 400 key: 3, value: 300 key: 0, value: 0 5 测试案例示意图 第 1 步:初始化数据结构。 第 2 步:插入节点 key1。 第 3 步:插入节点 key2。 此时 key2 插入到链表头部。 第 4 步:插入节点 key3。 此时 key3 插入到链表头部。 第 5 步:插入节点 key4。当前 capacity 容量达到上限(3),分为 2 步: 使用 removeTail() 方法删除链表尾部的节点 key1,从 removeTail() 方法的返回值得到 node,再根据 node.key 得到 key1,然后去哈希表删除节点 key1。 然后插入节点 key4,此时 key4 在链表头部。 第 6 步:读取 key2 的值,将 key2 移动到链表头部。 6 参考资料 [1] 146. LRU 缓存: https://leetcode.cn/problems/lru-cache/ [2] 以Leetcode第146题为例学习LRU缓存算法: https://mp.weixin.qq.com/s/nI-rp3zTtei3TFIoBDcr5Q [3] 从leetcode真题讲解手写LRU算法: http://www.xiaojieboshi.com/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/%E4%BB%8Eleetcode%E7%9C%9F%E9%A2%98%E8%AE%B2%E8%A7%A3%E6%89%8B%E5%86%99LRU%E7%AE%97%E6%B3%95.html#%E5%89%8D%E8%A8%80 [4] LRU缓存机制: https://leetcode.cn/problems/lru-cache/solution/lruhuan-cun-ji-zhi-by-leetcode-solution/ [5] Java集合系列之LinkedHashMap: https://juejin.cn/post/6844903544152129550 [6] LinkedHashMap基本原理和用法&使用实现简单缓存: https://www.cnblogs.com/myseries/p/10774487.html [7] LRU算法及其优化策略——算法篇: https://juejin.cn/post/6844904049263771662#heading-3 7 欢迎关注

优秀的个人博客,低调大师

每日一博 | 深入剖析 Spring WebFlux

一、WebFlux 简介 WebFlux 是 Spring Framework5.0 中引入的一种新的反应式Web框架。通过Reactor项目实现Reactive Streams规范,完全异步和非阻塞框架。本身不会加快程序执行速度,但在高并发情况下借助异步IO能够以少量而稳定的线程处理更高的吞吐,规避文件IO/网络IO阻塞带来的线程堆积。 1.1 WebFlux 的特性 WebFlux 具有以下特性: 异步非阻塞- 可以举一个上传例子。相对于 Spring MVC 是同步阻塞IO模型,Spring WebFlux这样处理:线程发现文件数据没传输好,就先做其他事情,当文件准备好时通知线程来处理(这里就是输入非阻塞方式),当接收完并写入磁盘(该步骤也可以采用异步非阻塞方式)完毕后再通知线程来处理响应(这里就是输出非阻塞方式)。 响应式函数编程- 相对于Java8 Stream 同步、阻塞的Pull模式,Spring Flux 采用Reactor Stream 异步、非阻塞Push模式。书写采用Javalambda 方式,接近自然语言形式且容易理解。 不拘束于Servlet- 可以运行在传统的Servlet 容器(3.1+版本),还能运行在Netty、Undertow等NIO容器中。 1.2 WebFlux 的设计目标 适用高并发 高吞吐量 可伸缩性 二、Spring WebFlux 组件介绍 2.1 HTTPHandler 一个简单的处理请求和响应的抽象,用来适配不同HTTP服务容器的API。 2.2 WebHandler 一个用于处理业务请求抽象接口,定义了一系列处理行为。相关核心实现类如下; 2.3 DispatcherHandler 请求处理的总控制器,实际工作是由多个可配置的组件来处理。 WebFlux是兼容Spring MVC 基于@Controller,@RequestMapping等注解的编程开发方式的,可以做到平滑切换。 2.4 Functional Endpoints 这是一个轻量级函数编程模型。是基于@Controller,@RequestMapping等注解的编程模型的替代方案,提供一套函数式API 用于创建Router,Handler和Filter。调用处理组件如下: 简单的RouterFuntion 路由注册和业务处理过程: @Bean public RouterFunction<ServerResponse> initRouterFunction() { return RouterFunctions.route() .GET("/hello/{name}", serverRequest -> { String name = serverRequest.pathVariable("name"); return ServerResponse.ok().bodyValue(name); }).build(); } 请求转发处理过程: 2.5 Reactive Stream 这是一个重要的组件,WebFlux 就是利用Reactor 来重写了传统Spring MVC 逻辑。其中Flux和Mono 是Reactor中两个关键概念。掌握了这两个概念才能理解WebFlux工作方式。 Flux和Mono 都实现了Reactor的Publisher接口,属于时间发布者,对消费者提供订阅接口,当有事件发生的时候,Flux或者Mono会通过回调消费者的相应的方法来通知消费者相应的事件。这就是所谓的响应式编程模型。 Mono工作流程图 只会在发送出单个结果后完成。 Flux工作流程图 发送出零个或者多个,可能无限个结果后才完成。 对于流式媒体类型:application/stream+json 或者 text/event-stream ,可以让调用端获得服务器滚动结果。 对于非流类型:application/json WebFlux 默认JSON编码器会将序列化的JSON 一次性刷新到网络,这并不意味着阻塞,因为结果Flux<?> 是以反应式方式写入网络的,没有任何障碍。 三、WebFlux 工作原理 3.1 组件装配过程 流程相关源码解析-WebFluxAutoConfiguration @Configuration //条件装配 只有启动的类型是REACTIVE时加载 @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) //只有存在 WebFluxConfigurer实例 时加载 @ConditionalOnClass(WebFluxConfigurer.class) //在不存在 WebFluxConfigurationSupport实例时 加载 @ConditionalOnMissingBean({ WebFluxConfigurationSupport.class }) //在之后装配 @AutoConfigureAfter({ ReactiveWebServerFactoryAutoConfiguration.class, CodecsAutoConfiguration.class, ValidationAutoConfiguration.class }) //自动装配顺序 @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10) public class WebFluxAutoConfiguration { @Configuration @EnableConfigurationProperties({ ResourceProperties.class, WebFluxProperties.class }) //接口编程 在装配WebFluxConfig 之前要先 装配EnableWebFluxConfiguration @Import({ EnableWebFluxConfiguration.class }) public static class WebFluxConfig implements WebFluxConfigurer { //隐藏部分源码 /** * Configuration equivalent to {@code @EnableWebFlux}. */ } @Configuration public static class EnableWebFluxConfiguration extends DelegatingWebFluxConfiguration { //隐藏部分代码 } @Configuration @ConditionalOnEnabledResourceChain static class ResourceChainCustomizerConfiguration { //隐藏部分代码 } private static class ResourceChainResourceHandlerRegistrationCustomizer implements ResourceHandlerRegistrationCustomizer { //隐藏部分代码 } WebFluxAutoConfiguration 自动装配时先自动装配EnableWebFluxConfiguration而EnableWebFluxConfiguration->DelegatingWebFluxConfiguration->WebFluxConfigurationSupport。 最终WebFluxConfigurationSupport 不仅配置DispatcherHandler 还同时配置了其他很多WebFlux核心组件包括 异常处理器WebExceptionHandler,映射处理器处理器HandlerMapping,请求适配器HandlerAdapter,响应处理器HandlerResultHandler 等。 DispatcherHandler 创建初始化过程如下; public class WebFluxConfigurationSupport implements ApplicationContextAware { //隐藏部分代码 @Nullable public final ApplicationContext getApplicationContext() { return this.applicationContext; } //隐藏部分代码 @Bean public DispatcherHandler webHandler() { return new DispatcherHandler(); } public class DispatcherHandler implements WebHandler, ApplicationContextAware { @Nullable private List<HandlerMapping> handlerMappings; @Nullable private List<HandlerAdapter> handlerAdapters; @Nullable private List<HandlerResultHandler> resultHandlers; @Override public void setApplicationContext(ApplicationContext applicationContext) { initStrategies(applicationContext); } protected void initStrategies(ApplicationContext context) { //注入handlerMappings Map<String, HandlerMapping> mappingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors( context, HandlerMapping.class, true, false); ArrayList<HandlerMapping> mappings = new ArrayList<>(mappingBeans.values()); AnnotationAwareOrderComparator.sort(mappings); this.handlerMappings = Collections.unmodifiableList(mappings); //注入handlerAdapters Map<String, HandlerAdapter> adapterBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors( context, HandlerAdapter.class, true, false); this.handlerAdapters = new ArrayList<>(adapterBeans.values()); AnnotationAwareOrderComparator.sort(this.handlerAdapters); //注入resultHandlers Map<String, HandlerResultHandler> beans = BeanFactoryUtils.beansOfTypeIncludingAncestors( context, HandlerResultHandler.class, true, false); this.resultHandlers = new ArrayList<>(beans.values()); AnnotationAwareOrderComparator.sort(this.resultHandlers); } **流程相关源码解析-**HTTPHandlerAutoConfiguration 上面已讲解过WebFlux核心组件装载过程,那么这些组件又是什么时候注入到对应的容器上下文中的呢?其实是在刷新容器上下文时注入进去的。 org.springframework.boot.web.reactive.context.ReactiveWebServerApplicationContext#onRefresh public class ReactiveWebServerApplicationContext extends GenericReactiveWebApplicationContext implements ConfigurableWebServerApplicationContext { @Override protected void onRefresh() { super.onRefresh(); try { createWebServer(); } catch (Throwable ex) { throw new ApplicationContextException("Unable to start reactive web server", ex); } } private void createWebServer() { WebServerManager serverManager = this.serverManager; if (serverManager == null) { String webServerFactoryBeanName = getWebServerFactoryBeanName(); ReactiveWebServerFactory webServerFactory = getWebServerFactory(webServerFactoryBeanName); boolean lazyInit = getBeanFactory().getBeanDefinition(webServerFactoryBeanName).isLazyInit(); // 这里创建容器管理时注入httpHandler this.serverManager = new WebServerManager(this, webServerFactory, this::getHttpHandler, lazyInit); getBeanFactory().registerSingleton("webServerGracefulShutdown", new WebServerGracefulShutdownLifecycle(this.serverManager)); // 注册一个 web容器启动服务类,该类继承了SmartLifecycle getBeanFactory().registerSingleton("webServerStartStop", new WebServerStartStopLifecycle(this.serverManager)); } initPropertySources(); } protected HttpHandler getHttpHandler() { String[] beanNames = getBeanFactory().getBeanNamesForType(HttpHandler.class); if (beanNames.length == 0) { throw new ApplicationContextException( "Unable to start ReactiveWebApplicationContext due to missing HttpHandler bean."); } if (beanNames.length > 1) { throw new ApplicationContextException( "Unable to start ReactiveWebApplicationContext due to multiple HttpHandler beans : " + StringUtils.arrayToCommaDelimitedString(beanNames)); } //容器上下文获取httpHandler return getBeanFactory().getBean(beanNames[0], HttpHandler.class); } 而这个HTTPHandler是由HTTPHandlerAutoConfiguration装配进去的。 @Configuration @ConditionalOnClass({ DispatcherHandler.class, HttpHandler.class }) @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) @ConditionalOnMissingBean(HttpHandler.class) @AutoConfigureAfter({ WebFluxAutoConfiguration.class }) @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10) public class HttpHandlerAutoConfiguration { @Configuration public static class AnnotationConfig { private ApplicationContext applicationContext; public AnnotationConfig(ApplicationContext applicationContext) { this.applicationContext = applicationContext; } //构建WebHandler @Bean public HttpHandler httpHandler() { return WebHttpHandlerBuilder.applicationContext(this.applicationContext) .build(); } } 流程相关源码解析-web容器 org.springframework.boot.web.reactive.context.ReactiveWebServerApplicationContext#createWebServer。在创建WebServerManager 容器管理器时会获取对应web容器实例,并注入响应的HTTPHandler。 class WebServerManager { private final ReactiveWebServerApplicationContext applicationContext; private final DelayedInitializationHttpHandler handler; private final WebServer webServer; WebServerManager(ReactiveWebServerApplicationContext applicationContext, ReactiveWebServerFactory factory, Supplier<HttpHandler> handlerSupplier, boolean lazyInit) { this.applicationContext = applicationContext; Assert.notNull(factory, "Factory must not be null"); this.handler = new DelayedInitializationHttpHandler(handlerSupplier, lazyInit); this.webServer = factory.getWebServer(this.handler); } } 以Tomcat 容器为例展示创建过程,使用的是 TomcatHTTPHandlerAdapter 来连接Servlet 请求到HTTPHandler组件。 public class TomcatReactiveWebServerFactory extends AbstractReactiveWebServerFactory implements ConfigurableTomcatWebServerFactory { //隐藏部分代码 @Override public WebServer getWebServer(HttpHandler httpHandler) { if (this.disableMBeanRegistry) { Registry.disableRegistry(); } Tomcat tomcat = new Tomcat(); File baseDir = (this.baseDirectory != null) ? this.baseDirectory : createTempDir("tomcat"); tomcat.setBaseDir(baseDir.getAbsolutePath()); Connector connector = new Connector(this.protocol); connector.setThrowOnFailure(true); tomcat.getService().addConnector(connector); customizeConnector(connector); tomcat.setConnector(connector); tomcat.getHost().setAutoDeploy(false); configureEngine(tomcat.getEngine()); for (Connector additionalConnector : this.additionalTomcatConnectors) { tomcat.getService().addConnector(additionalConnector); } TomcatHttpHandlerAdapter servlet = new TomcatHttpHandlerAdapter(httpHandler); prepareContext(tomcat.getHost(), servlet); return getTomcatWebServer(tomcat); } } 最后Spring容器加载后通过SmartLifecycle实现类WebServerStartStopLifecycle 来启动Web容器。 WebServerStartStopLifecycle 注册过程详见:org.springframework.boot.web.reactive.context.ReactiveWebServerApplicationContext#createWebServer 3.2 完整请求处理流程 (引用自:https://blog.csdn.net) 该图给出了一个HTTP请求处理的调用链路。是采用Reactor Stream 方式书写,只有最终调用 subscirbe 才真正执行业务逻辑。基于WebFlux开发时要避免controller 中存在阻塞逻辑。列举下面例子可以看到Spring MVC 和Spring Webflux 之间的请求处理区别。 @RestControllerpublic class TestController { private Logger logger = LoggerFactory.getLogger(this.getClass()); @GetMapping("sync") public String sync() { logger.info("sync method start"); String result = this.execute(); logger.info("sync method end"); return result; } @GetMapping("async/mono") public Mono<String> asyncMono() { logger.info("async method start"); Mono<String> result = Mono.fromSupplier(this::execute); logger.info("async method end"); return result; } private String execute() { try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } return "hello"; } } 日志输出 2021-05-31 20:14:52.384 INFO 3508 --- [nio-8080-exec-2] c.v.internet.webflux.web.TestController : sync method start 2021-05-31 20:14:57.385 INFO 3508 --- [nio-8080-exec-2] c.v.internet.webflux.web.TestController : sync method end 2021-05-31 20:15:09.659 INFO 3508 --- [nio-8080-exec-3] c.v.internet.webflux.web.TestController : async method start 2021-05-31 20:15:09.660 INFO 3508 --- [nio-8080-exec-3] c.v.internet.webflux.web.TestController : async method end 从上面例子可以看出sync() 方法阻塞了请求,而asyncMono() 没有阻塞请求并立刻返回的。asyncMono() 方法具体业务逻辑 被包裹在了Mono 中Supplier中的了。当execute 处理完业务逻辑后通过回调方式响应给浏览器。 四、存储支持 一旦控制层使用了 Spring Webflux 则安全认证层、数据访问层都必须使用 Reactive API 才真正实现异步非阻塞。 NOSQL Database MongoDB(org.springframework.boot:spring-boot-starter-data-mongodb-reactive)。 Redis(org.springframework.boot:spring-boot-starter-data-redis-reactive)。 Relational Database H2(io.r2dbc:r2dbc-h2) MariaDB(org.mariadb:r2dbc-mariadb) Microsoft SQL Server(io.r2dbc:r2dbc-mssql) MySQL(dev.miku:r2dbc-mysql) jasync-sql MySQL(com.github.jasync-sql:jasync-r2dbc-mysql) Postgres(io.r2dbc:r2dbc-postgresql) Oracle(com.oracle.database.r2dbc:oracle-r2dbc) 五、总结 关于Spring MVC 和Spring WebFlux 测评很多,本文引用下做简单说明。参考:《Spring: Blocking vs non-blocking: R2DBC vs JDBC and WebFlux vs Web MVC》。 基本依赖 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-r2dbc</artifactId> </dependency> <!-- r2dbc 连接池 --> <dependency> <groupId >io.r2dbc</groupId> <artifactId>r2dbc-pool</artifactId> </dependency> <!--r2dbc mysql 库--> <dependency> <groupId>dev.miku</groupId> <artifactId>r2dbc- mysql</artifactId> </dependency> <!--自动配置需要引入一个嵌入式数据库类型对象--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jdbc</artifactId> </dependency> <!-- 反应方程式 web 框架 webflux--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency> 相同数据下效果如下**;** Spring MVC + JDBC 在低并发下表现最好,但 WebFlux + R2DBC 在高并发下每个处理请求使用的内存最少。 Spring WebFlux + R2DBC 在高并发下,吞吐量表现优异。 作者:vivo互联网服务器团队-Zhou Changqing

资源下载

更多资源
腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

用户登录
用户注册